PolynomialHQdocs
Guides

Undo and redo

Add local mutation history controls to collaborative storage.

Storage mutations are added to the room's local history. Each connection can undo or redo its own mutation snapshots without changing another user's history stack.

Loading the history demo…

Add controls

import {
  useCanRedo,
  useCanUndo,
  useRedo,
  useUndo,
} from "@polynomialhq/react";

export function HistoryControls() {
  const canRedo = useCanRedo();
  const canUndo = useCanUndo();
  const redo = useRedo();
  const undo = useUndo();

  return (
    <div>
      <button disabled={!canUndo} onClick={undo} type="button">
        Undo
      </button>
      <button disabled={!canRedo} onClick={redo} type="button">
        Redo
      </button>
    </div>
  );
}

Use the history object

useHistory() returns the imperative history interface:

const history = useHistory();

history.canUndo();
history.canRedo();
history.undo();
history.redo();
history.clear();

RoomHistory has exactly these five members. The framework-agnostic room exposes the same object at room.history, and room.getHistorySnapshot() returns the { canUndo, canRedo } pair that useCanUndo() and useCanRedo() read.

What enters history

Calls created with useMutation() or room.createMutation() capture the storage document before the mutation. Presence-only updates are not durable document history.

const renameDocument = useMutation<Presence, Storage, [string]>(
  ({ storage }, title) => {
    storage.setAt(["title"], title);
  },
  [],
);

After renameDocument("Launch brief"), undo() restores the previous storage snapshot and redo() reapplies the new snapshot.

A mutation is one history entry no matter how many writes it performs, so group the writes that belong to a single user action into a single mutation. Writes made outside a mutation — by calling room.getStorage().setAt(…) directly — each become their own entry.

Undo restores the whole document

History stores full snapshots of the storage document, not per-field operations. Undoing therefore replays this connection's previous document, which can also revert a change a collaborator made in the meantime. In the demo above, edit from both windows and then undo in one of them to see it.

For documents that several people edit at the same moment, scope the undoable surface: keep independently-owned data in separate rooms, or drive undo from your own domain-level operations instead of the room history.

Redo is discarded as soon as a new mutation runs, and history.clear() empties both stacks. The stacks are per-connection and in-memory, so a reload starts with an empty history.

Keyboard shortcuts

useEffect(() => {
  function onKeyDown(event: KeyboardEvent) {
    const modifier = event.metaKey || event.ctrlKey;
    if (!modifier || event.key.toLowerCase() !== "z") {
      return;
    }

    event.preventDefault();
    if (event.shiftKey) {
      redo();
    } else {
      undo();
    }
  }

  window.addEventListener("keydown", onKeyDown);
  return () => window.removeEventListener("keydown", onKeyDown);
}, [redo, undo]);

Only register document-level shortcuts while the collaborative editor is active. Let native text inputs handle their own undo stack.

On this page