# Research Notes — Undo Isn't the Opposite of Do
### Designing History for Async React · Full research for course & talk

**Primary source (the event):** [Frontendistim Community August Meetup at Matia](https://www.meetup.com/frontendistim-meetup-group/events/315986879/) — Thu, Aug 27 2026, 18:00–21:00 IDT, Matia @ Beit Gibor Sport, Menachem Begin 7, Ramat Gan. Hosted by Nir P. (Nir Parisian), organized by Enpitech.

**The talk's own abstract** (verbatim from the event page):

> "Undo looks like a stack until one click starts several state changes, waits on the network, overlaps another click, and touches data too large to snapshot repeatedly. In this talk, we will see how to build Undo/Redo for a React 19 drag-and-drop dashboard and deliberately pass through four attractive designs: full snapshots, Immer patches, and commands with explicit inverse behavior, before arriving at a transactional, forward-only replay model. Along the way we will make the failures visible: memory growth, history polluted by internal mutations, non-serializable commands, late async completions, and ambiguous batching. We will finish with semantic events, transaction IDs assigned at intent time, atomic publication, closure-free snapshots, and a rolling checkpoint that bounds memory. The goal is not to prescribe one pattern, but to give you a practical way to choose the unit your users actually expect Undo to reverse."

---

## 1. The core thesis (what the talk argues)

Undo is not `state[n-1]`. It is a **product decision about the unit of reversibility**, and the architecture follows from the unit:

| Failure (talk) | Architectural disease | Verified mechanic |
|---|---|---|
| Memory growth | Storing whole states per step (Memento-style snapshots) | O(n) copies; impossible for large trees |
| History polluted by internal mutations | Ephemeral state (drag position, hover, focus) stored in the same tree as committed state | Need `partialize`/selection of what history tracks |
| Non-serializable commands | Command objects capturing closures over mutable scope | Closures capture environment; can't persist/replay |
| Late async completions | `onSuccess` writes to history after user already undid | Need txId check at completion time |
| Ambiguous batching | One user intent = N dispatched mutations, each recorded separately | Need transaction ID assigned at **intent time** |

---

## 2. Design 1 — Full Snapshots (Memento pattern)

**Verified source:** [refactoring.guru — Memento](https://refactoring.guru/design-patterns/memento)

- Intent (verbatim): *"Memento is a behavioral design pattern that lets you save and restore the previous state of an object without revealing the details of its implementation."*
- Also known as: **Snapshot**. Structure: **Originator** (produces/restores snapshots) + **Memento** (immutable value snapshot) + **Caretaker** (knows *when/why* to capture; stores the stack).
- Classic undo implementation: capture a copy of state before each operation, push onto history stack.

**Where it fails (the talk's visible failures):**
- **Memory growth**: every entry is a full copy. A dashboard with large document trees (thousands of nodes) → each drag gesture copies the whole tree. Redux-DevTools-era apps literally hit this; that's why devtools store *actions*, not states.
- **Pollution**: if the captured tree includes ephemeral fields (isDragging, hoverId), every mouse move becomes an undo step — the history is garbage.
- **Success mode**: only when state is small, immutable, and strictly partitioned (committed vs ephemeral).

**Mitigations that exist (verified):**
- **Zundo** ([github.com/charkour/zundo](https://github.com/charkour/zundo)) — "🍜 undo/redo middleware for zustand. <700 B": ships `partialize` (choose *which fields* history tracks — fixes pollution) and `limit` (cap history size — bounds memory). Confirms both failures are real and commonly solved at library level.

## 3. Design 2 — Immer Patches (structural sharing via diff)

**Verified source:** [immerjs.github.io — Patches](https://immerjs.github.io/immer/patches)

- Since Immer v6, patches require an explicit `enablePatches()` call once at app startup.
- `produceWithPatches(reducer, state)` returns the tuple **`[nextState, patches, inversePatches]`** (verbatim from docs).
- `applyPatches(state, patches)` replays a patch list — docs explicitly market this for undo/redo: *egghead lessons 19/20: "Using inverse patches to build undo functionality", "Use patches to build redo functionality"*.
- Patches are **minimal** (`{op: "replace"|"add"|"remove", path: [...], value}`) — memory per step is proportional to the *change*, not the state. Structural sharing (Immer's core) means unchanged branches are reused.

**Where it fails:**
- Patches replay onto the *exact* tree they were recorded against — diverge (e.g. another actor changed the same branch, or a late async completion mutates) and `applyPatches` breaks or silently corrupts.
- Patch history is a low-level diff log, not a *semantic* record: you can undo data, but you can't undo *meaning* (a patch log has no notion of "this drag gesture").
- Inverse patches are only correct if the producer was **pure** — side effects or external data reads make them lie.

## 4. Design 3 — Commands with Explicit Inverse (Command pattern)

**Verified source:** [refactoring.guru — Command](https://refactoring.guru/design-patterns/command)

- Intent (verbatim): *"Command is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as method arguments, delay or queue a request's execution, and support **undoable operations**."*
- The pattern's own example is a text editor with undo: commands that mutate state make a backup copy, execute, then join a **command history** stack. Undo = pop + call `undo()`.
- ProseMirror implements exactly this at scale ([prosemirror.net/docs/guide](https://prosemirror.net/docs/guide/)): its `transform` module provides *"modifying documents in a way that can be recorded and replayed, which is the basis for the transactions in the state module, and which makes the undo history and collaborative editing possible."* Steps have `invert()` — the inverse is a **derived function of the step itself**, not a closure over execution state.

**Where it fails:**
- **Non-serializable commands**: the naive version stores `{do: () => {...}, undo: () => {...}}` — closures over component scope. Can't be persisted, can't be inspected, can't survive refresh, and leak whatever they captured. The fix (per ProseMirror): commands are *plain data* + a registry of interpreters; the inverse is computed from data.
- Inverse correctness is now your job: every command needs a verified inverse, and inverses of async commands (network writes) are themselves async and can fail — you need compensating actions, not inverses.

## 5. Design 4 — Transactional Forward-Only Replay (event sourcing applied to UI history)

**Verified source:** [Martin Fowler — Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)

Fowler's three facilities (verbatim concepts):
1. **Complete Rebuild** — *"discard the application state completely and rebuild it by re-running the events from the event log"*.
2. **Temporal Query** — *"determine the application state at any point in time... rerunning the events up to a particular time or event"*.
3. **Event Replay** — *"if a past event was incorrect, compute the consequences by reversing it and later events and then replaying"*.

- Fowler explicitly names **version control** as the canonical event-sourcing application — the same mental model as undo/redo.
- Forward-only replay means: history is an **append-only log of semantic events**; the current state is a *projection* of the log; undo = re-project from the last checkpoint up to event N−1. There is no "inverse" at all — undo and redo are the **same operation** (replay to a different log position). That's the deep point of the talk's title: undo is not the opposite of do, it's do, shorter.

**The talk's five mechanisms (from the abstract) mapped to real techniques:**

| Mechanism | What it means | Analogue |
|---|---|---|
| Semantic events | Log `USER_MOVED_CARD {cardId, to}` not `{op:replace, path:[0].x}` | Domain events in ES |
| Transaction ID at **intent time** | txId minted on pointer-down / click, before any awaits | Idempotency key / saga correlation id |
| Atomic publication | One user intent → one append, even if it causes N state mutations | Transactional outbox pattern (microservices.io) |
| Closure-free snapshots | Snapshots/events are plain serializable data | Event payloads are DTOs, not closures |
| Rolling checkpoint | Every K events, compact: snapshot + truncate prefix | Log compaction (Kafka), VCS snapshots |

**Late async completions**: a network write resolves *after* the user pressed undo. With replay: the completion handler checks `txId === headOfLog` — if not, the result is applied as a *new* event or discarded, never spliced into history behind the cursor. With snapshots/patches: the late mutation rewrites state that the history no longer describes → corruption or ghost states. This is the **React 19 relevance**: actions/transitions make multiple concurrent async updates first-class, which is exactly when naive undo breaks.

**Verified React 19 hook (for the course's React integration chapter):** [react.dev — useTransition](https://react.dev/reference/react/useTransition)
- `const [isPending, startTransition] = useTransition()` — *"a React Hook that lets you render a part of the UI in the background"*, returns `isPending` flag + `startTransition` for non-blocking updates with Actions.

## 6. DnD context for the demo dashboard

**Verified source:** [docs.dndkit.com](https://docs.dndkit.com/)
- dnd-kit: "The modern toolkit for building drag and drop interfaces" — framework-agnostic (React/Vue/Svelte/Solid), concepts: `DragDropManager`, `Draggable`, `Droppable`, `Sortable`, plus `Sensors` and `Modifiers`.
- Why DnD is the perfect undo laboratory (talk's own choice): a single drag gesture is one *user intent* but emits dozens of internal state updates (position every frame, over/leave events) — the perfect trap for snapshot engines, the perfect case for tx-based replay. We mirror this in the course lab.

## 7. What the course adds beyond the talk (research-grounded)

1. **The Unit Ladder** (synthesis, not in any single source): pick the unit of undo first — *keystroke → field → gesture/transaction → document version* — then the pattern picks itself:
   - Small unit + small state → snapshots are fine (and simplest)
   - Unit = delta, single actor → patches
   - Unit = operation with known inverse → commands
   - Unit = user intent spanning async/network → transactional replay
2. **Instrumentation as design tool**: every design in the lab shows live memory bytes + history entries, making Fowler's "memory growth" and the talk's "failures visible" tangible.
3. **React 19 specifics**: transitions (`startTransition`), `useOptimistic`, actions — how async React changed undo's assumptions (an update is no longer synchronous single-dispatch).

## 8. Source list (all verified by direct fetch on 2026-08-16)

1. Meetup event page — https://www.meetup.com/frontendistim-meetup-group/events/315986879/ (abstract, venue, host)
2. Immer Patches docs — https://immerjs.github.io/immer/patches (`produceWithPatches`, `applyPatches`, inverse patches, enablePatches)
3. ProseMirror Guide — https://prosemirror.net/docs/guide/ (recorded/replayable transforms → undo history + collab)
4. Fowler, Event Sourcing — https://martinfowler.com/eaaDev/EventSourcing.html (rebuild, temporal query, replay; VCS analogy)
5. React docs, useTransition — https://react.dev/reference/react/useTransition
6. Refactoring Guru, Command — https://refactoring.guru/design-patterns/command (undoable operations, history stack)
7. Refactoring Guru, Memento — https://refactoring.guru/design-patterns/memento (snapshot, originator/caretaker)
8. Zundo — https://github.com/charkour/zundo (partialize, limit, <700 B middleware)
9. dnd-kit docs — https://docs.dndkit.com/ (toolkit, concepts)

*Not cited (couldn't verify at primary source during research window): Redux DevTools README (repo reorganized; action-replay behavior is well known but was left out to keep every claim primary-verified).*
