diff --git a/lib/shared/search-utils.js b/lib/shared/search-utils.js
--- a/lib/shared/search-utils.js
+++ b/lib/shared/search-utils.js
@@ -1,12 +1,19 @@
 // @flow
 
+import * as React from 'react';
+
 import SearchIndex from './search-index.js';
 import {
   userIsMember,
   threadMemberHasPermission,
   getContainingThreadID,
 } from './thread-utils.js';
+import {
+  searchMessages,
+  searchMessagesActionTypes,
+} from '../actions/message-actions.js';
 import genesis from '../facts/genesis.js';
+import type { SearchMessagesResponse } from '../types/message-types.js';
 import { userRelationshipStatus } from '../types/relationship-types.js';
 import {
   type ThreadInfo,
@@ -15,6 +22,10 @@
   threadPermissions,
 } from '../types/thread-types.js';
 import type { AccountUserInfo, UserListItem } from '../types/user-types.js';
+import {
+  useServerCall,
+  useDispatchActionPromise,
+} from '../utils/action-utils.js';
 
 const notFriendNotice = 'not friend';
 
@@ -172,4 +183,32 @@
   );
 }
 
-export { getPotentialMemberItems, notFriendNotice };
+function useSearchMessages(
+  query: string,
+  threadID: string,
+  cursor?: string,
+): SearchMessagesResponse {
+  const [searchResults, setSearchResults] = React.useState([]);
+  const [endReached, setEndReached] = React.useState(false);
+
+  const callSearchMessages = useServerCall(searchMessages);
+  const dispatchActionPromise = useDispatchActionPromise();
+
+  React.useEffect(() => {
+    const searchMessagesPromise = (async () => {
+      const { messages, endReached: end } = await callSearchMessages({
+        query,
+        threadID,
+        cursor,
+      });
+      setSearchResults(messages);
+      setEndReached(end);
+    })();
+
+    dispatchActionPromise(searchMessagesActionTypes, searchMessagesPromise);
+  }, [callSearchMessages, query, threadID, cursor, dispatchActionPromise]);
+
+  return { messages: searchResults, endReached };
+}
+
+export { getPotentialMemberItems, notFriendNotice, useSearchMessages };