# react-amnesia > Long-form AI retrieval export for the canonical react-amnesia documentation. ## Canonical source pages - AI Overview: https://thirtytwobits.github.io/react-amnesia/docs/ai - Invariants: https://thirtytwobits.github.io/react-amnesia/docs/ai/invariants - Decision Matrix: https://thirtytwobits.github.io/react-amnesia/docs/ai/decision-matrix - Recipes: https://thirtytwobits.github.io/react-amnesia/docs/ai/recipes - Anti-Patterns: https://thirtytwobits.github.io/react-amnesia/docs/ai/anti-patterns - AI Assistant Setup: https://thirtytwobits.github.io/react-amnesia/docs/ai/assistant-setup ## Machine-readable companion - https://thirtytwobits.github.io/react-amnesia/ai-contract.json - https://thirtytwobits.github.io/react-amnesia/llms.txt ## Quick rules - `useAmnesia(...)`, `useUndoableState(...)`, `useAmnesiaFocusClaim(...)`, `useAmnesiaScopes(...)`, and `` must run inside an `AmnesiaProvider`. - The undo stack is in-memory only. Persist the value (e.g. via `react-mnemonic`) — never the closures. - `Command.do` / `redo` / `undo` may be sync or async. `push` / `undo` / `redo` always return `Promise`. - Each handler receives an `AbortSignal`. `clear()` and `dispose()` abort it; pass it to `fetch` for clean cancellation. - An AbortError thrown after `signal.aborted === true` is a silent no-op. A handler that ignores the signal still drops the commit but fires `onError({ phase: "stale" })`. - The store is single-flight. Concurrent ops while `pending === true` resolve to `null` with `onError({ phase: "busy" })`. - `push({ redo, undo, label })` calls `redo()` once on insertion. Pass `{ applied: true }` when state is already mutated. - Use `coalesceKey` for keystroke / drag bursts so a single Ctrl+Z reverts the whole burst. - A new `push` clears the redo (future) stack — branching is not supported. - `useUndoableState` returns `[value, set, reset]`. `reset(next?)` clears the bound scope's history; it is not undoable. - Multi-scope: one provider, many independent stores. `useAmnesiaFocusClaim(scopeId)` routes Ctrl+Z to the focused scope. - Transactions collapse N pushes into one composite entry; throw inside `work` runs every buffered undo in reverse. - Lifecycle hooks (`onPush` / `onUndo` / `onRedo` / `onClear`) are post-notify, microtask-deferred, and re-entrant-safe. - `metaTransform` redacts `meta` everywhere it leaves the store — snapshot AND hooks. - `` defaults to `skipEditableTargets: true` and walks `composedPath()` to recognize editables in shadow roots. - DevTools registry (`enableDevTools`) is opt-in and lazy-installed; nothing in the bundle activates unless a provider sets it. - Import published values and types from `react-amnesia`, not internal paths or local ambient shims. ## AI Overview Source: https://thirtytwobits.github.io/react-amnesia/docs/ai # AI Overview This section is the authoritative, high-signal contract for humans and coding assistants using `react-amnesia`. Use it when you need: - application undo/redo semantics without reading the whole repo - a reliable rule for `push`, `undo`, `redo`, `clear`, and coalescing - the shortest correct explanation of capacity, error handling, and keyboard binding - guidance for combining `react-amnesia` with `react-mnemonic` for persisted-yet-undoable state - copy-pastable patterns that stay aligned with the public API ## Quick Start The minimum correct shape is: mount an `AmnesiaProvider` above every component that calls `useAmnesia(...)`, `useUndoableState(...)`, or `usePersistedUndoableState(...)`. Render exactly one `` per provider for keyboard bindings. ```tsx // main.tsx import React from "react"; import ReactDOM from "react-dom/client"; import { AmnesiaProvider, AmnesiaShortcuts } from "react-amnesia"; import { App } from "./App"; ReactDOM.createRoot(document.getElementById("root")!).render( , ); ``` Any descendant of `App` can now call `useUndoableState(...)` for reversible single-value state, `useAmnesia()` for direct access to the history store, or `push({ redo, undo, label })` for imperative commands. ## Start Here Read these pages in order when context is tight: 1. [Invariants](./ai/invariants) 2. [Decision Matrix](./ai/decision-matrix) 3. [Recipes](./ai/recipes) 4. [Anti-Patterns](./ai/anti-patterns) 5. [AI Assistant Setup](./ai/assistant-setup) ## Quick Rules - React 18 or 19 are supported peers (`^18.0.0 || ^19.0.0`); both are exercised in CI under ``. - `useAmnesia(...)`, `useAmnesiaLabels(...)`, `useUndoableState(...)`, `useAmnesiaFocusClaim(...)`, `useAmnesiaScopes(...)`, and `` must run inside an `AmnesiaProvider`. - `usePersistedUndoableState(...)` from `react-amnesia/mnemonic` must run inside both an `AmnesiaProvider` and a `MnemonicProvider`. - A provider owns multiple **scopes**, each an independent `Amnesia` store. The implicit `"default"` scope exists; named scopes are created lazily on first reference. - `useAmnesia()` (no arg) tracks the currently active scope and re-renders when active changes. `useAmnesia("canvas")` pins to a named scope. - `useAmnesiaLabels(scopeId?)` follows the same scope resolution as `useAmnesia`, but returns only `{ canUndo, canRedo, undoLabel, redoLabel, pending, scopeId }` for menu/toolbar bindings and avoids re-renders when those fields are unchanged. - `useUndoableState(initial, { scopeId })` and `usePersistedUndoableState(...)` pin to a named scope (default `"default"`); they do **not** float to the active claim. - `useAmnesiaFocusClaim(scopeId)` returns capture-phase focus / pointer-down handlers that mark a surface as the active claimant. The handlers go on a focusable container element. - At most one focused-child claim is held at a time. The most recently claimed scope wins; on claim-component unmount, the active falls back to default if the unmounting component held the claim. - `` routes Ctrl+Z / Cmd+Z to the active scope by default. Pin with `` to ignore claim changes. - `` calls `event.preventDefault()` whenever the chord matches outside an editable target — even when there is nothing to undo. This is required because async `undo` / `redo` cannot synchronously decide whether the browser's native handler should run. - Per-scope option overrides go on the provider: ``. Settings are read at scope-creation time (lazy). - `useAmnesiaScopes()` returns `{ activeScopeId, scopeIds, clear(scopeId?) }` for provider-level UI (breadcrumbs, document-switch reset). `clear()` with no arg clears every scope; `clear("canvas")` clears one. - The undo stack is **in-memory only**. Closures are not serialized and the history does not survive a reload. - To survive reloads, persist the underlying value (e.g. via `react-mnemonic`) and let the history start fresh per session. - `Command.redo` and `Command.undo` may be synchronous or return `Promise`. `push` / `amend` / `undo` / `redo` always return `Promise`. - `push({ redo, undo, label })` calls `redo()` once on insertion. Pass `{ applied: true }` when the call site has already mutated state. - `amend({ ...patch })` updates only the latest past entry (`past[past.length - 1]`). Omitted fields are preserved, `undo` is preserved unless explicitly replaced, and future is cleared. - `Command.do` is optional. When supplied, it runs once at push time instead of `redo` and is **not stored** on the entry — every subsequent redo invokes `command.redo`. Use `do` when first-apply requires setup that re-apply does not. - `useAmnesia(scopeId?).transaction(label?, work)` collapses N pushes into one composite entry. `tx.push(command)` runs `command.do ?? command.redo` immediately and buffers `command.redo` / `command.undo`. On commit the past stack gains exactly one composite entry whose redo/undo replay all buffered handlers in order / reverse order. - A throw inside a transaction's `work` rolls back every buffered undo in reverse and re-throws to the caller. `clear()` / `dispose()` during the await stales the transaction and rolls back instead. Empty transactions resolve to `null` with no entry. - Nested `transaction(...)` calls flatten into the outermost; the nested `label` argument is ignored, the outermost label or any `tx.label(...)` call wins. - Composite entries never coalesce with stack neighbors. Inside a transaction, individual `tx.push` calls do not coalesce with each other either. - Lifecycle hooks (`onPush` / `onAmend` / `onUndo` / `onRedo` / `onClear`) are provider-level options. They fire once per logical action — coalesce-merges and rollback-due-to-throw do not fire `onPush`. A transaction commit fires exactly one `onPush` for the composite entry. - Hook payloads carry `(entry, scopeId)`; `onClear` carries `(scopeId)`. Hooks fire after subscribers have been notified, so handlers see a quiescent store and may safely re-enter `push` / `undo` / `redo`. A throwing hook is caught and ignored. - `metaTransform: (meta) => meta | undefined` redacts `meta` before it reaches the snapshot or any hook. Use this to strip secrets / PII without forcing every call site to remember the rule. Returning `undefined` strips meta entirely; a throwing transform also strips it. - `useUndoableState` returns `[value, set, reset]`. `reset()` restores the value captured on first render and clears the bound scope's history. `reset(next)` overrides with a specific value. Reset is **not undoable** — the scope is wiped. - `usePersistedUndoableState` returns `{ value, set, reset, remove }`. `reset(next?)` is composite: it clears the history scope AND restores the persisted value via `react-mnemonic` (calling `mnemonic.reset()` with no arg, `mnemonic.set(next)` otherwise). `remove()` deletes the persisted key AND clears the history scope. - `reset` and `remove` clear the **entire scope** the hook is bound to — including entries from sibling hooks or imperative pushes that share the same scope. Pin sensitive history to its own `scopeId` when that boundary matters. - `` registers the provider with `window.__REACT_AMNESIA_DEVTOOLS__`. The registry is opt-in and lazy-installed: when no provider sets `enableDevTools`, no global is created. - The devtools api exposes `id`, `getActiveScopeId()`, `scopes()`, `getSnapshot(scopeId?)`, `pastSnapshot(scopeId?)`, `futureSnapshot(scopeId?)`, `dump()`, `triggerUndo(scopeId?)`, `triggerRedo(scopeId?)`, and `clear(scopeId?)`. External tooling and AI agents can introspect or drive a live store without touching application code. - Provider entries are held weakly via `WeakRef` when available, so a long-lived registry never prevents an unmounted provider from being garbage-collected. - Every command handler (`do` / `redo` / `undo`) and every transaction `work` function receives an `AbortSignal` argument. The signal aborts when `clear()` or `dispose()` runs while the handler is in flight. Pass it to `fetch` (which cancels the network call) or check `signal.aborted` in long loops. A rejection thrown after `signal.aborted === true` is treated as a silent no-op — no `onError` event fires and the entry is dropped. - Handlers that ignore the signal still drop the commit via the existing epoch check; the difference is that `onError({ phase: "stale" })` fires for ignored signals and stays silent for honored ones. - Each operation gets its own `AbortController` — sibling ops don't share signals. Nested transactions DO share the outer transaction's signal, so cancellation propagates through the whole flattened buffer. - A new `push` clears the redo (future) stack. There is no branching in v0. - Use `coalesceKey` (e.g. `"edit:title"`) for keystroke or drag bursts so a single Ctrl+Z reverts the whole burst. Coalescing across async commands is fragile — recommend against it. - Two pushes coalesce only when they share the same non-empty `coalesceKey` and pass the effective coalescing window for that push (`command.coalesceWindowMs` override or scope default). - Capacity defaults to `100`. When the limit is reached, the oldest past entry is dropped silently — do not rely on history for audit trails. - `clear()` is synchronous. It drops both stacks, bumps the `epoch` counter, empties the pending set, and notifies subscribers once. - `` defaults to `skipEditableTargets: true` so the browser's native undo handles text-like `` types plus `