Skip to content

Undo / Redo

StateMesh tracks state history automatically. Every state change captures a snapshot, and undo() / redo() restore previous / next states.

Enable Undo/Redo

ts
const mesh = createMesh({
  state: { cart: { items: [], count: 0 } },
  undo: {
    maxHistory: 50,       // Max undo entries (default: 50)
    paths: ["cart"]       // Optional: only track these paths
  }
});
OptionDefaultDescription
maxHistory50Maximum undo entries retained. Oldest entries are evicted first.
pathsundefinedOptional array of state paths to track. When omitted, the full state is tracked. Reduces memory by cloning only tracked paths.

API

ts
mesh.undo();              // Restore the previous state
mesh.redo();              // Restore the next state (undone by undo)

mesh.canUndo;             // true when undo stack has entries
mesh.canRedo;             // true when redo stack has entries
mesh.undoStackSize;       // Current undo stack depth
mesh.redoStackSize;       // Current redo stack depth
mesh.clearUndoHistory();  // Clear both stacks

Usage

ts
mesh.setPath("cart.items", [product]);
mesh.setPath("cart.items", [product, accessory]);

mesh.undo();    // Restores cart.items to [product]
mesh.redo();    // Restores cart.items to [product, accessory]

mesh.canUndo;   // true
mesh.canRedo;   // true

Batch-Aware

Multiple state changes inside mesh.batch() are captured as a single undo entry:

ts
mesh.batch(() => {
  mesh.setPath("cart.items", [...items, newItem]);
  mesh.setPath("cart.count", items.length + 1);
});

mesh.undo(); // Reverts both changes at once

Reset-Aware

mesh.reset() pushes the pre-reset state to the undo stack:

ts
mesh.setPath("theme", "dark");
mesh.reset();   // Resets to initial state

mesh.undo();    // Restores theme to "dark"

Path Filtering

Track only specific paths to reduce memory usage:

ts
const mesh = createMesh({
  state: {
    theme: "light",
    user: { name: "Alice", email: "alice@example.com" },
    cart: { items: [] as Item[] }
  },
  undo: {
    paths: ["cart"]  // Only track cart changes
  }
});

mesh.setPath("theme", "dark");     // NOT tracked (not in paths)
mesh.setPath("cart.items", [item]); // Tracked

mesh.undo();  // Reverts cart.items, theme stays "dark"

Events

Undo/redo emit state.changed events with metadata.phase set to "undo" or "redo":

ts
mesh.on({ type: "state.changed" }, (event) => {
  if (event.metadata?.phase === "undo") {
    console.log("Undo performed");
  }
  if (event.metadata?.phase === "redo") {
    console.log("Redo performed");
  }
});

React Example

tsx
import { useMesh } from "statemesh-core";

function UndoRedoControls() {
  const mesh = useMesh();

  return (
    <div>
      <button disabled={!mesh.canUndo} onClick={() => mesh.undo()}>
        Undo
      </button>
      <button disabled={!mesh.canRedo} onClick={() => mesh.redo()}>
        Redo
      </button>
      <span>History: {mesh.undoStackSize} / 50</span>
      <button onClick={() => mesh.clearUndoHistory()}>Clear</button>
    </div>
  );
}

Important Notes

TIP

Undo/redo is opt-in. No memory or CPU cost unless you enable it in createMesh.

WARNING

Each undo entry stores a deep clone of the tracked state. With maxHistory: 50 and full-state tracking, this can use significant memory for large state trees. Use paths to limit tracking.

TIP

clearUndoHistory() frees all stored snapshots. Call it when undo history is no longer needed (e.g., after a successful save).

Next Steps

Released under the MIT License.