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.
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.