All questions

Undo / Redo Manager II

Premium

Undo / Redo Manager II

Build a state manager that powers undo and redo the way a real editor does — where a burst of edits counts as one Ctrl+Z step, not one per keystroke. The user types a sentence, drags a shape, pastes a block; each of those is many small changes, but one undo should revert the whole burst. Your manager separates two layers: a draft that accumulates uncommitted edits, and a stack of committed checkpoints that undo/redo navigate between. The draft is where typing happens; a commit() is the moment the editor decides "this is one undoable unit."

Signature

class UndoRedoManager {
  // Optional initial committed state. Defaults to {}.
  constructor(initialState?: Record<string, unknown>);

  set(key: string, value: unknown): void; // write to the draft; no undo step
  get(key: string): unknown;              // read the effective (draft) value
  commit(): void;                         // snapshot the draft as ONE checkpoint
  undo(): Record<string, unknown>;        // step back, return the restored state
  redo(): Record<string, unknown>;        // step forward, return the restored state
  canUndo(): boolean;
  canRedo(): boolean;
}

Examples

// Several sets, ONE commit → a single undo reverts all of them.
const m = new UndoRedoManager({ x: 0, y: 0, z: 0 });
m.set('x', 1);
m.set('y', 2);
m.set('z', 3); // all three live in the draft, no undo step yet
m.commit();    // the burst becomes ONE checkpoint
m.undo();      // one step reverts the whole batch
m.get('x');    // → 0
m.get('y');    // → 0
m.canUndo();   // → false  (back at the start)
// A commit AFTER an undo invalidates the redo stack.
const m = new UndoRedoManager({ title: 'a' });
m.set('title', 'b');
m.commit();          // checkpoints: a → b, pointer at b
m.undo();            // pointer back at a; redo would return 'b'
m.canRedo();         // → true
m.set('title', 'c');
m.commit();          // diverged: the 'b' branch is discarded
m.canRedo();         // → false
m.get('title');      // → 'c'

Notes

  • Batching is the whole point. Many set calls before a commit collapse into a single undoable step. set never pushes a checkpoint on its own — only commit does.
  • A divergent commit truncates the redo stack. After an undo, committing new work abandons the forward branch you stepped off of. You cannot redo into history you've diverged from.
  • Draft vs. committed. get always reads the draft (the effective current value). undo/redo operate on committed checkpoints: each rebuilds the draft from the checkpoint it lands on. An uncommitted draft is not itself an undo step — commit it first if you want it preserved.
  • A no-op commit adds nothing. If the draft is identical to the current checkpoint, commit does not push a step (so undo doesn't stop on phantom checkpoints).
  • Boundaries are safe. undo at the oldest checkpoint and redo at the newest are no-ops that return the current state rather than throwing.
  • Snapshots are independent. Mutating a state object returned by undo/redo must not corrupt stored history.
  • Don't worry about deleting keys, concurrent commits, or persisting history across sessions — those are out of scope.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium