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

The flow store

Read and change the live graph from any component rendered inside KookieFlow, without re-rendering on every frame.

KookieFlow keeps the live graph in a Zustand store: the entities and edges as the canvas holds them, the viewport, the selection, and the indexes it uses to find things quickly. Any component you render inside KookieFlow can read that store and change it.

During a drag or a pan the store changes on every frame. Read it when you need a value, and subscribe to the one piece you care about, so your components don't re-render sixty times a second.

Reach the store from inside KookieFlow

Render your component as a child of KookieFlow, and call useFlowStoreApi() in it. Children are drawn in a layer over the canvas.

flow.tsx
'use client';import { KookieFlow, useGraph } from '@kushagradhawan/kookie-flow';import { SelectionPanel } from './selection-panel';export function Flow() {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph();  return (    <div style={{ height: 480 }}>      <KookieFlow        entities={entities}        edges={edges}        onEntitiesChange={onEntitiesChange}        onEdgesChange={onEdgesChange}        onConnect={onConnect}      >        <div style={{ position: 'absolute', top: 12, right: 12, pointerEvents: 'auto' }}>          <SelectionPanel />        </div>      </KookieFlow>    </div>  );}
  • Set pointerEvents: 'auto' on anything people click. The layer that holds children lets the pointer through to the canvas, so a button without it can't be pressed.
  • Most canvas keys don't reach your controls. Keys like Delete and the arrows work only while the canvas itself has focus, so typing in an input in your panel doesn't delete nodes. The undo shortcut also respects inputs and editable content inside the canvas.
  • Outside KookieFlow, use the ref. useFlowStoreApi() throws when it's called in a component that isn't rendered inside KookieFlow, including the component that renders it. The ref instance covers what you can do from there.

useFlowStoreApi() returns a FlowStore. You use two of its methods: getState(), which returns the current FlowState, and subscribe(). Change the store through the actions on getState() rather than setState(), because the actions also rebuild the indexes that clicks and hovers rely on.

Read state in an event handler

getState() returns the state at the moment you call it. It doesn't subscribe and it doesn't render, so it's the right call inside a click handler.

selection-panel.tsx
'use client';import { useFlowStoreApi } from '@kushagradhawan/kookie-flow';export function SelectionPanel() {  const store = useFlowStoreApi();  const copyAsJson = () => {    const { getSelectedEntities, getConnectedEdges, selectedEntityIds } = store.getState();    const entities = getSelectedEntities();    const edges = getConnectedEdges([...selectedEntityIds]);    void navigator.clipboard.writeText(JSON.stringify({ entities, edges }, null, 2));  };  return <button onClick={copyAsJson}>Copy selection as JSON</button>;}

These are the fields you read most often. The last column says how often each one changes, which matters as soon as you subscribe to it.

FieldTypeChanges
entitiesEntity[]A live, store-owned array. Structural/data edits replace the array; drag/resize replaces affected elements in place. Subscribe to positionVersion for movement.
edgesEdge[]When edges are added, removed or replaced.
viewportViewportOn every frame of a pan or zoom.
selectedEntityIdsSet<string>When the entity selection changes.
selectedEdgeIdsSet<string>When the edge selection changes.
entityMapMap<string, Entity>A new map whenever the entities are rebuilt, such as when your entities prop changes. During a drag it's updated in place, so a subscriber misses the moves. Read it, don't subscribe to it.
hoveredEntityIdstring | nullWhen the pointer moves onto or off an entity.
positionVersionnumberGoes up when a drag, a resize or updateEntityPositions moves entities, and whenever your entities prop changes.
topologyVersionnumberGoes up when entities or edges are added or removed, whenever your entities or edges prop changes, and when an entity is muted or unmuted.
hiddenEntityIdsSet<string>Holds the entities inside collapsed frames. A new set whenever the entities are rebuilt.
focusedEntityIdstring | nullThe entity the keyboard cursor is on. Changes when someone presses an entity, moves the cursor with a key, or when the canvas adds an entity for a dropped or pasted file or link.

The query actions are in Graph queries, and copying and pasting is in Clipboard and cloning. The rest of the store's actions are listed further down this page.

Subscribe to one piece of state

store.subscribe(selector, listener) calls listener whenever the value selector returns changes, and returns a function that unsubscribes. Values are compared with Object.is. Pass { equalityFn } as a third argument to compare them another way, or { fireImmediately: true } to call listener once straight away.

For anything that changes during a gesture, update the page through a ref, and do it at most once per frame. This readout follows the zoom while someone zooms, without rendering React once:

zoom-readout.tsx
'use client';import { useEffect, useRef } from 'react';import { useFlowStoreApi } from '@kushagradhawan/kookie-flow';export function ZoomReadout() {  const store = useFlowStoreApi();  const labelRef = useRef<HTMLSpanElement>(null);  useEffect(() => {    let frame = 0;    const paint = () => {      frame = 0;      const { zoom } = store.getState().viewport;      if (labelRef.current) labelRef.current.textContent = `${Math.round(zoom * 100)}%`;    };    paint();    const unsubscribe = store.subscribe(      (state) => state.viewport.zoom,      () => {        if (frame === 0) frame = requestAnimationFrame(paint);      }    );    return () => {      unsubscribe();      cancelAnimationFrame(frame);    };  }, [store]);  return <span ref={labelRef} />;}

When a value changes only when someone clicks, a render is fine. useFlowStore(selector) re-renders your component whenever the selected value changes:

import { useFlowStore } from '@kushagradhawan/kookie-flow';export function SelectionCount() {  const count = useFlowStore((state) => state.selectedEntityIds.size);  return <span>{count} selected</span>;}

Two rules keep useFlowStore from costing you frames:

  • Don't select viewport or positionVersion for interaction readouts. Each changes during a gesture, so the component would render on every frame. Subscribe with a ref instead.
  • An entities selector observes array replacements, not drag/resize. Read the current entity from entityMap in a positionVersion subscriber to follow movement.
  • Return a value that stays the same when nothing changed: a number, a string, a boolean, or a reference the store already holds. A selector that builds a new array or object on every call never returns the same value twice, and React renders again and again.

Add and delete elements

addElements and deleteElements change several entities and edges in one update.

ActionTakesWhat it does
addElementsElementsBatchAdds entities and edges. Each entity gets its type's sockets, size and label from entityTypes, and is drawn above what's already there.
deleteElementsDeleteElementsBatchRemoves entities and edges by id, along with every edge attached to a removed entity, and takes them out of the selection.
deleteSelectednothingRemoves the selected entities and edges, the same way.
interface ElementsBatch {  entities?: Entity[];  edges?: Edge[];}interface DeleteElementsBatch {  entityIds?: string[];  edgeIds?: string[];}

The store also holds actions for selection, the camera and positions:

ActionSignatureWhat it does
selectEntity(id: string, additive?: boolean) => voidSelects one entity. Without additive, it replaces the selection and clears selected edges.
selectEntities(ids: string[]) => voidReplaces the entity selection. Selected edges stay selected.
toggleEntitySelection(id: string) => voidAdds an entity to the selection, or takes it out.
selectEdge(id: string, additive?: boolean) => voidSelects one edge. Without additive, it replaces the selection and clears selected entities.
selectEdges(ids: string[]) => voidReplaces the edge selection.
selectAll() => voidSelects every entity.
deselectAll() => voidClears the entity and edge selection.
isEntitySelected(id: string) => booleanWhether the entity is selected.
isEdgeSelected(id: string) => booleanWhether the edge is selected.
setViewport(viewport: Viewport) => voidMoves the camera.
pan(delta: XYPosition) => voidMoves the graph on screen by delta pixels. A positive x moves it right.
zoom(delta: number, center?: XYPosition) => voidAdds delta to the zoom, kept between 0.01 and 16 whatever your minZoom and maxZoom props say. Pass center, a point on the canvas in pixels, to keep the graph under that point in place.
updateEntityPositions(updates: Array<{ id: string; position: XYPosition }>) => voidMoves entities without rebuilding the whole graph.
updateEntityDimensions(id: string, width: number, height: number, position?: XYPosition) => voidSets an entity's width and height, and its position when you pass one, without rebuilding the whole graph.
fitEntityToContent(id: string) => voidRemoves an entity's width and height, so the canvas sizes it as it sizes an entity with neither set.
applyEntityChanges(changes: EntityChange[]) => voidApplies the same changes onEntitiesChange receives.
applyEdgeChanges(changes: EdgeChange[]) => voidApplies the same changes onEdgesChange receives.

Move frames, restack and re-run entities

These actions work on frames, on the order entities are drawn in, and on evaluation:

ActionSignatureWhat it does
moveGroup(groupId: string, delta: XYPosition) => voidMoves an entity and everything inside it, at every depth, by delta world units.
setEntityParent(entityId: string, parentId: string | null) => booleanSets the entity's parentId, or clears it with null. Nothing moves. Returns false and changes nothing when either entity is missing, when both ids are the same, or when the parent is already inside the entity.
bringToFront(entityId: string) => voidDraws the entity above the others, as a press does. The frames around it come up beneath it, and a frame's contents come up above the frame.
markDirty(ids: string | readonly string[]) => voidMarks the entities, and everything downstream of them, dirty. A run in progress on any of them is cancelled, and reactive entities run again.

markDirty is for a change the canvas can't see, such as a file or a remote value one of your entities reads. Evaluation covers what dirty means and what runs.

bringToFront and markDirty change only what the store holds, like selection and the camera, so they work in any graph. moveGroup, setEntityParent, updateEntityDimensions and fitEntityToContent change entities without reporting it, so the next section applies to them.

The rest of FlowState serves the canvas's own gestures: hovering, drawing a wire, editing text, pressing a widget and drawing with the pen. Those fields change in step with the gesture, and calling one of the actions on its own can leave the gesture half done. stopEditing, for example, closes the text editor without reporting what was typed, and setWidgetValue changes what a widget shows without calling onWidgetChange.

Edit a controlled graph through your own state

Store actions change the canvas, and they don't call onEntitiesChange or onEdgesChange. The canvas also replaces its entities whenever your entities prop changes, and its edges whenever your edges prop changes. So in a controlled graph, an edit made through the store lasts only until your state next changes.

Here's how that goes wrong. A child calls addElements(), and the new node appears. Someone drags another node. useGraph applies that drag to its own array, which never had the new node, and the new node disappears.

In a controlled graph, add, delete and move entities through your own state. Pass the handlers from useGraph to the child that needs them:

add-node-button.tsx
'use client';import {  screenToWorld,  useFlowStoreApi,  type Entity,  type EntityChange,} from '@kushagradhawan/kookie-flow';export function AddNodeButton({ onEntitiesChange }: { onEntitiesChange: (changes: EntityChange[]) => void }) {  const store = useFlowStoreApi();  const add = () => {    const entity: Entity = {      id: crypto.randomUUID(),      type: 'default',      // 40 pixels in from the top-left corner of what's on screen.      position: screenToWorld({ x: 40, y: 40 }, store.getState().viewport),      data: { label: 'New node' },    };    onEntitiesChange([{ type: 'add', entity }]);  };  return <button onClick={add}>Add node</button>;}

Render it as <AddNodeButton onEntitiesChange={onEntitiesChange} /> inside KookieFlow. The change goes through useGraph, so it survives the next render, and with history on, undo can take it back.

Selection and the camera exist only in the store, so selection and viewport actions are safe in a controlled graph. If your entities and edges arrays never change identity, the store is the only copy of the graph, and its edits last. The Controlled and uncontrolled state chapter explains both setups.

Retain snapshots and address sockets

getState().entities is live. Use toObject() for a graph snapshot rather than retaining that array across moves. Input arrays supplied by your app are not mutated by drag/resize.

Use socketValueKey(entityId, socketId) or entitySocketKey(entityId, socketId) for flat value maps, and connectedSocketKey(entityId, socketId, isInput) for connectedSockets and SocketIndexMap. The helpers escape delimiters in entity ids, so ids containing : or % do not alias a different entity/socket pair. Avoid constructing these keys by concatenation.

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
  • Reach the store from inside KookieFlow
  • Read state in an event handler
  • Subscribe to one piece of state
  • Add and delete elements
  • Move frames, restack and re-run entities
  • Edit a controlled graph through your own state
  • Retain snapshots and address sockets