diff --git a/lib/shared/edit-messages-utils.js b/lib/shared/edit-messages-utils.js
--- a/lib/shared/edit-messages-utils.js
+++ b/lib/shared/edit-messages-utils.js
@@ -1,14 +1,55 @@
 // @flow
 
+import * as React from 'react';
+
 import { threadHasPermission } from './thread-utils.js';
+import {
+  sendEditMessageActionTypes,
+  sendEditMessage,
+} from '../actions/message-actions.js';
 import { messageTypes } from '../types/message-types.js';
 import type {
   RobotextMessageInfo,
   ComposableMessageInfo,
 } from '../types/message-types.js';
 import { threadPermissions, type ThreadInfo } from '../types/thread-types.js';
+import {
+  useDispatchActionPromise,
+  useServerCall,
+} from '../utils/action-utils.js';
+import { cloneError } from '../utils/errors.js';
 import { useSelector } from '../utils/redux-utils.js';
 
+function useEditMessage(messageID?: string): (newText: string) => mixed {
+  const callEditMessage = useServerCall(sendEditMessage);
+  const dispatchActionPromise = useDispatchActionPromise();
+
+  return React.useCallback(
+    newText => {
+      if (!messageID) {
+        return;
+      }
+
+      const editMessagePromise = (async () => {
+        try {
+          const result = await callEditMessage({
+            targetMessageID: messageID,
+            text: newText,
+          });
+          return {
+            newMessageInfos: result.newMessageInfos,
+          };
+        } catch (e) {
+          throw cloneError(e);
+        }
+      })();
+
+      dispatchActionPromise(sendEditMessageActionTypes, editMessagePromise);
+    },
+    [messageID, dispatchActionPromise, callEditMessage],
+  );
+}
+
 function useCanEditMessage(
   threadInfo: ThreadInfo,
   targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo,
@@ -36,4 +77,4 @@
   return hasPermission;
 }
 
-export { useCanEditMessage };
+export { useCanEditMessage, useEditMessage };