Getting started
InstallationQuick startControlled and uncontrolled state
Entities
The entity modelSockets and widgetsEntity typesFrames and groupsText and commentsImages, video and models
Edges and connections
EdgesConnections
Data
EvaluationSaving and exportingUndo and redoLayout and alignmentThe flow storeGraph queriesClipboard and cloning
Interaction
Canvas and cameraSelectionKeyboardSelection toolbarContext menusDrawingDropping and pasting media
Styling
ThemingEntity stylingFonts and text renderingMinimap
API reference
KookieFlowThe ref instanceHooksUtilitiesTypes
StartEntitiesDataAPI
MarkdownOpenAIClaude

Undo and redo

Turn on undo and redo in useGraph, add buttons for them, and learn what counts as one step.

useGraph can keep an undo history of the graph it holds. Turn it on with one option, and you get undo and redo functions, two flags for enabling buttons, and Cmd/Ctrl+Z on the canvas.

Turn on history

Pass history: true to useGraph.

board.tsx
'use client';import { KookieFlow, useGraph, type Edge, type Entity } from '@kushagradhawan/kookie-flow';export function Board({ initialEntities, initialEdges }: { initialEntities: Entity[]; initialEdges: Edge[] }) {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({    initialEntities,    initialEdges,    history: true,  });  return (    <KookieFlow      entities={entities}      edges={edges}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={onConnect}    />  );}

History is off by default. An app that already keeps its own history, in a document store, on a server or in a collaborative document, would otherwise end up with two, both answering Cmd/Ctrl+Z.

OptionTypeDefaultMeaning
historyboolean | UseGraphHistoryOptionsfalsetrue keeps 50 steps and binds the keyboard. An object sets either.

useGraph returns four fields for history.

FieldTypeMeaning
undo() => voidPuts the graph back one step. Does nothing when there's no step.
redo() => voidMoves forward one step again. Does nothing when there's no step.
canUndobooleanWhether there's a step to undo.
canRedobooleanWhether there's a step to redo.

With history off, undo and redo do nothing, and canUndo and canRedo stay false.

What counts as one step

Before useGraph applies a batch of changes, it keeps a copy of the whole graph, its entities and its edges. Undo restores the most recent copy, and redo returns to where you were before the undo. These rules decide when a copy is kept:

  • A selection isn't a step. A batch that only selects or deselects records nothing, so clicking around never fills the history.
  • A drag is one step. The canvas reports a move once, when the pointer is released, however many frames the drag took. A resize works the same way.
  • An edit that keeps arriving is one step. Two batches are folded together when both are moves, both are resizes or both are data changes, both touch exactly the same entities, and the second arrives less than 600 milliseconds after the first. A quick run of Shift+arrow nudges is one step, and so is a slider drag that your onWidgetChange reports as a data change. Moving one node and then another makes two steps, and so does moving a node and then resizing it. A batch that mixes kinds is never folded: a resize from a top or left handle reports a resize and a move together, so each one is a step of its own.
  • Adding and removing are never folded. A batch that adds or removes entities or edges is always a step of its own, and so is each new wire. Deleting a node with wires attached is one step, although the canvas reports the wires and the node separately.
  • A ref method that reports its changes is recorded. alignSelection and distributeSelection report their moves through onEntitiesChange, as a drag does, so they're recorded the same way.
  • Undoing starts a new step. The next edit after an undo or a redo isn't folded into an earlier one, and it discards the steps you could have redone.

Only changes that arrive through onEntitiesChange, onEdgesChange and onConnect are recorded. setEntities, setEdges, addEntity, removeEntity, addEdge and removeEdge change the graph without recording a step. An undo after one of them restores a copy taken before it, so that change is undone along with the step. Report an edit through onEntitiesChange when people should be able to undo it.

Widget values are the edit this catches most often. The canvas reports a widget change only to onWidgetChange, and writing it back with setEntities records nothing. Report it as a data change instead:

board.tsx
'use client';import { useCallback } from 'react';import { KookieFlow, useGraph, type Entity } from '@kushagradhawan/kookie-flow';export function Board({ initialEntities }: { initialEntities: Entity[] }) {  const { entities, edges, getEntity, onEntitiesChange, onEdgesChange, onConnect } = useGraph({    initialEntities,    history: true,  });  const onWidgetChange = useCallback(    (entityId: string, socketId: string, value: unknown) => {      const current = getEntity(entityId)?.data.values;      const values = { ...(current instanceof Object ? current : {}), [socketId]: value };      onEntitiesChange([{ type: 'data', id: entityId, data: { values } }]);    },    [getEntity, onEntitiesChange]  );  return (    <KookieFlow      entities={entities}      edges={edges}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={onConnect}      onWidgetChange={onWidgetChange}    />  );}

History holds the graph and nothing else. The viewport, the selection and computed values aren't part of a step. After an undo, any entity whose data.values changed is marked dirty, and a reactive one runs again, as described in Evaluation.

Add undo and redo buttons

Bind the buttons' disabled state to canUndo and canRedo.

Arrow keys move between nodes. Press Enter to reach the focused node's controls, and Escape to come back here.

'use client';import { useEffect, useRef } from 'react';import { Button, Flex, Stack } from '@kushagradhawan/kookie-ui-react';import { KookieFlow, useGraph, type Edge, type Entity, type KookieFlowInstance } from '@kushagradhawan/kookie-flow';const node = (id: string, x: number, y: number): Entity => ({  id,  type: 'default',  position: { x, y },  data: { label: id },  inputs: [{ id: 'in', name: 'In', type: 'image' }],  outputs: [{ id: 'out', name: 'Out', type: 'image' }],});const wire = (source: string, target: string): Edge => ({  id: `${source}-${target}`, source, sourceSocket: 'out', target, targetSocket: 'in',});export default function HistoryExample() {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect, undo, redo, canUndo, canRedo } = useGraph({    initialEntities: [node('Load', 0, 60), node('Blur', 300, 0), node('Sharpen', 300, 160), node('Save', 600, 60)],    initialEdges: [wire('Load', 'Blur'), wire('Blur', 'Save')],    // Fifty steps, and Cmd/Ctrl+Z while the canvas has focus.    history: true,  });  const flowRef = useRef<KookieFlowInstance>(null);  useEffect(() => {    flowRef.current?.fitView({ padding: 40, maxZoom: 1 });  }, []);  return (    <Stack gap="3">      {/* Drag a node, wire Load into Sharpen, or delete one, then undo. Clicking to select adds no step. */}      <Flex gap="2">        <Button size="2" onClick={undo} disabled={!canUndo}>Undo</Button>        <Button size="2" onClick={redo} disabled={!canRedo}>Redo</Button>      </Flex>      <div style={{ width: '100%', height: 360 }}>        <KookieFlow          ref={flowRef}          entities={entities}          edges={edges}          onEntitiesChange={onEntitiesChange}          onEdgesChange={onEdgesChange}          onConnect={onConnect}        />      </div>    </Stack>  );}

Drag a node, wire Load into Sharpen, or select a node and press Delete, then press Undo. Click between nodes as much as you like first: selecting adds no steps.

Change the limit and the shortcuts

Pass an object instead of true to change either default. Its type is UseGraphHistoryOptions.

OptionTypeDefaultMeaning
limitnumber50How many steps are kept. Past the limit, the oldest step is dropped.
shortcutsbooleantrueBinds Cmd/Ctrl+Z to undo, and Cmd/Ctrl+Shift+Z and Cmd/Ctrl+Y to redo, while the canvas has focus.

The shortcuts affect only the graph whose canvas has focus. Text fields and editable content, including controls inside the canvas, keep their native undo. Events another handler has already handled are ignored.

Pass the returned onEntitiesChange directly to KookieFlow so it can identify its history owner. If you wrap that handler, or use a custom canvas, pass history: { containerRef } with a React ref to that graph's focusable container. Each graph needs its own container.

To bind the keys yourself, turn shortcuts off first. Both listeners would otherwise run, and one key press would undo two steps. This board binds them with useKeyboardShortcuts, which listens on the whole page except while a text field has focus:

board.tsx
'use client';import { KookieFlow, useGraph, type Entity } from '@kushagradhawan/kookie-flow';import { useKeyboardShortcuts } from '@kushagradhawan/kookie-flow/plugins';export function Board({ initialEntities }: { initialEntities: Entity[] }) {  const { undo, redo, entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({    initialEntities,    history: { limit: 200, shortcuts: false },  });  useKeyboardShortcuts({    bindings: { 'mod+z': undo, 'mod+shift+z': redo, 'mod+y': redo },  });  return (    <KookieFlow      entities={entities}      edges={edges}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={onConnect}    />  );}

The Keyboard chapter covers useKeyboardShortcuts in full.

Keep your own history instead

When your graph lives in another store, such as Redux, Zustand or a server cache, leave history off and record steps where your store applies changes. A collaborative document usually comes with an undo manager of its own, and that one is the right place.

This small history follows the same rules as useGraph for selection and for Delete's two batches. read returns the graph from your store, and write puts one back.

graph-history.ts
import type { Edge, EdgeChange, Entity, EntityChange } from '@kushagradhawan/kookie-flow';type Graph = { entities: Entity[]; edges: Edge[] };export function createGraphHistory(read: () => Graph, write: (graph: Graph) => void, limit = 50) {  const past: Graph[] = [];  let future: Graph[] = [];  let batchOpen = false;  /** Keep a copy of the graph as it is now. Call it before adding the edge in onConnect. */  const checkpoint = () => {    // Delete reports wires, then the node, in the same tick: that's one step.    if (batchOpen) return;    batchOpen = true;    queueMicrotask(() => { batchOpen = false; });    past.push(read());    if (past.length > limit) past.shift();    future = [];  };  return {    checkpoint,    /** Call with every batch from onEntitiesChange and onEdgesChange, before you apply it. */    record: (changes: Array<EntityChange | EdgeChange>) => {      if (changes.some((change) => change.type !== 'select')) checkpoint();    },    undo: () => {      const previous = past.pop();      if (!previous) return;      future.unshift(read());      write(previous);    },    redo: () => {      const next = future.shift();      if (!next) return;      past.push(read());      write(next);    },  };}

A slider sends a data change for every value it passes through. If your app writes those back, fold them the way useGraph does: skip the checkpoint while data changes to the same entity keep arriving less than 600 milliseconds apart.

Getting started
  • Installation
  • Quick start
  • Controlled and uncontrolled state
Entities
  • The entity model
  • Sockets and widgets
  • Entity types
  • Frames and groups
  • Text and comments
  • Images, video and models
Edges and connections
  • Edges
  • Connections
Data
  • Evaluation
  • Saving and exporting
  • Undo and redo
  • Layout and alignment
  • The flow store
  • Graph queries
  • Clipboard and cloning
Interaction
  • Canvas and camera
  • Selection
  • Keyboard
  • Selection toolbar
  • Context menus
  • Drawing
  • Dropping and pasting media
Styling
  • Theming
  • Entity styling
  • Fonts and text rendering
  • Minimap
API reference
  • KookieFlow
  • The ref instance
  • Hooks
  • Utilities
  • Types
Reference
  • Live demo
  • llms.txt
  • GitHub
MIT licensed. © 2026 Kushagra Dhawan.
On this page
  • Turn on history
  • What counts as one step
  • Add undo and redo buttons
  • Change the limit and the shortcuts
  • Keep your own history instead