# How Instatic Editor History Uses Mutative for Undo: Implementation Guide

> Learn how Instatic editor history uses Mutative for undo. Discover how patches are captured and pushed to a history stack for fine-grained, coalesced undo operations.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-31

---

**Instatic implements undo functionality by wrapping Mutative drafts in a `runHistoricMutation` helper that captures patches and pushes them to a per-step history stack integrated with Y.UndoManager, enabling coalesced, fine-grained undo operations.**

The CoreBunch/Instatic repository implements a robust editor history system by combining the Mutative library with Zustand middleware. This architecture captures every state change as a set of patches, enabling precise undo and redo operations while integrating seamlessly with Yjs collaborative documents.

## Architecture Overview: Mutative and Zustand Integration

The foundation of Instatic's undo system lies in the global editor store defined in [`src/admin/pages/site/store/store.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/store.ts). Lines 59‑63 initialize the Zustand store with the `mutative` middleware from `zustand-mutative`, which provides a draft-based mutation API and automatically records Mutative patches for every state change.

This setup allows state slices to mutate drafts directly while the middleware handles immutability behind the scenes. When paired with the history management layer, these mutations generate the patch data required for reversible undo steps.

## The Mutation Pipeline: Capturing Patches with runHistoricMutation

All undo-aware state changes flow through `runHistoricMutation`, a helper defined in [`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts) at lines 164‑170. This function serves as the bridge between Mutative's draft-based API and the editor's history stack.

The helper accepts a recipe function that receives a `Draft<EditorStore>` and an optional `coalesceKey`. Internally, it calls `mutative.create` to execute the recipe against a draft, capturing both the result and the generated patches. These patches represent the precise changes made to the state tree.

```typescript
// Core helper – runs a mutation and records a single undo step
function runHistoricMutation<T>(
  recipe: (draft: Draft<EditorStore>) => T,
  coalesceKey: string | null = null
): T {
  // `create` from Mutative returns patches + the result of the recipe
  const { result, patches } = create(recipe);
  // Store patches in undo history (via collab binding)
  pushUndoStep(patches, coalesceKey);
  return result;
}

```

Slices such as `site` use this wrapper to ensure every mutation automatically registers with the undo system without manual patch management.

### Coalescing Rapid Edits

To prevent flooding the history stack with micro-edits (such as individual keystrokes), the `coalesceKey` parameter enables logical grouping. When multiple mutations share the same `coalesceKey`, the system merges their patches into a single undo entry. This ensures that a burst of rapid typing collapses into one user-visible undo step rather than dozens.

## History Management and Yjs Integration

Once `runHistoricMutation` generates patches, they are passed to the collaboration binding layer in [`src/admin/pages/site/store/slices/site/collabBinding.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/collabBinding.ts) (lines 88‑119). Here, the `pushUndoStep` function stores patches in a per-step undo route map that tracks which Yjs documents (`Y.Doc`) were affected by each change.

This design separates the patch generation (handled by Mutative) from the document synchronization (handled by Yjs). Lines 310‑326 in the same file manage the cleanup and routing logic, ensuring that undo steps correctly map back to their originating collaborative documents.

## Executing Undo and Redo Operations

The actual undo and redo logic resides in [`src/admin/pages/site/store/slices/site/undoRedoActions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/undoRedoActions.ts) at lines 15‑22. When `undo()` is invoked, it retrieves the stored route of affected documents from the undo stack, then calls `Y.UndoManager.undo()` for each corresponding Y-Doc to revert the changes.

```typescript
// Undo action implementation (simplified)
export const undo = () => {
  // Gather docs that changed in the last step
  const docsToUndo = undoRoute.pop() ?? [];
  docsToUndo.forEach((docId) => {
    const manager = managed.get(docId)?.manager;
    manager?.undo();               // Y.UndoManager performs the revert
  });
  // Refresh UI after undo
  updateStoreFromDocs();
};

```

The `redo()` function performs the inverse operation using the same route logic, reapplying patches that were previously reverted. This two-step process—Mutative for state drafting and Yjs for operational transformation—ensures consistency across local and collaborative contexts.

## React Hook Integration

Components access the undo capability through the `useUndo` hook exported from [`src/admin/pages/site/store/store.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/store.ts) at lines 272‑278. This stable selector provides direct access to the `undo` action, enabling keyboard shortcuts like CMD+Z to trigger history traversal efficiently.

```tsx
// Hook used by UI components
export const useUndo = () => useEditorStore((s) => s.undo);

// Example of an undo‑aware mutation (adding a new page)
function addNewPage(name: string) {
  // `mutateSite` is a wrapper around `runHistoricMutation`
  mutateSite((draft) => {
    // `draft.site` is a Mutative draft of the whole site
    draft.site!.pages.push({
      id: generateId(),
      title: name,
      nodes: [],
    });
  });
}

```

## Summary

- **Mutative integration**: The Zustand store in [`store.ts`](https://github.com/CoreBunch/Instatic/blob/main/store.ts) uses `mutative` middleware to enable draft-based mutations with automatic patch generation.
- **Centralized mutation helper**: `runHistoricMutation` in [`helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/helpers.ts) wraps all state changes, capturing patches and managing coalescing via `coalesceKey`.
- **Yjs coordination**: The collab binding layer routes patches to `Y.UndoManager` instances, maintaining synchronization between local undo history and collaborative documents.
- **Action execution**: [`undoRedoActions.ts`](https://github.com/CoreBunch/Instatic/blob/main/undoRedoActions.ts) implements the actual traversal of history routes, invoking `Y.UndoManager.undo()` and `redo()` on affected documents.
- **React interface**: The `useUndo` hook provides components with direct access to undo functionality without prop drilling.

## Frequently Asked Questions

### What is Mutative and why does Instatic use it for undo?

Mutative is a JavaScript library that provides immutable updates via draft mutations, similar to Immer but with enhanced performance characteristics. Instatic uses Mutative because it generates granular patches automatically, which the editor history system can capture and store for precise reversibility without requiring developers to manually track diffs.

### How does coalescing work in Instatic's undo system?

Coalescing groups multiple rapid mutations into a single undo entry. When calling `runHistoricMutation`, developers can provide a `coalesceKey` string. Consecutive mutations sharing the same key have their patches merged in the history stack, ensuring that operations like continuous typing generate one undo step rather than individual character-level steps.

### What role does Y.UndoManager play in the Instatic editor?

`Y.UndoManager` from the Yjs library manages the operational transformation layer for collaborative editing. Instatic routes Mutative-generated patches through `Y.UndoManager` instances bound to specific Y-Docs, allowing the editor to undo changes consistently across both local state and synchronized collaborative documents.

### How do React components trigger undo in Instatic?

Components import the `useUndo` hook from [`src/admin/pages/site/store/store.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/store.ts). This hook returns the `undo` action function, which can be bound to keyboard events (such as CMD+Z) or UI buttons. When invoked, it executes the undo logic defined in [`undoRedoActions.ts`](https://github.com/CoreBunch/Instatic/blob/main/undoRedoActions.ts), walking the stored history route and reverting changes via `Y.UndoManager`.