Zustand Stores
Behave-Graph Flow uses Zustand for state management, providing a lightweight, performant, and React-agnostic solution. The architecture employs multiple specialized stores, each responsible for a specific domain of the editor state.
Why Zustand?
Section titled “Why Zustand?”Zustand was chosen for several key advantages:
- No Provider Hell: Direct store access without wrapping components in providers
- Fine-Grained Subscriptions: Components subscribe only to data they need
- Framework Agnostic: Can be used outside React (testing, automation)
- TypeScript First: Excellent type inference and safety
- Minimal Boilerplate: Simple, clean API
- Middleware Support: Easy to add logging, persistence, devtools
Store Architecture Overview
Section titled “Store Architecture Overview”Flow uses ~15 specialized stores, each managing a specific concern:
system.flowStore // Nodes, edges, viewportsystem.selectionStore // Selected elementssystem.variableStore // Graph variablessystem.eventsStore // Custom eventssystem.actionStore // Actions and commandssystem.tabStore // Panel layoutsystem.menubarStore // Menu structuresystem.controlStore // Input controlssystem.registryStore // Node metadatasystem.legendStore // Node type legendsystem.logsStore // Debug logssystem.hotKeyStore // Keyboard shortcutssystem.nodeStore // Node-specific statesystem.edgeStore // Edge-specific statesystem.traceStore // Execution tracesCore Stores
Section titled “Core Stores”FlowStore
Section titled “FlowStore”Manages the graph structure: nodes, edges, and viewport.
interface FlowStore { // State nodes: Node[]; edges: Edge[]; viewport: Viewport;
// Actions addNode(node: Node): void; removeNode(nodeId: string): void; updateNode(nodeId: string, data: Partial<Node>): void;
addEdge(edge: Edge): void; removeEdge(edgeId: string): void;
setViewport(viewport: Viewport): void;
// Computed getNodeById(id: string): Node | undefined; getEdgesByNodeId(nodeId: string): Edge[]; getGraph(): GraphJSON; // Serialize to runtime format setGraph(graph: GraphJSON): void; // Deserialize}Usage:
const FlowComponent = () => { const system = useSystem();
// Subscribe to all nodes const nodes = useStore(system.flowStore, (s) => s.nodes);
// Subscribe to specific derived data const nodeCount = useStore(system.flowStore, (s) => s.nodes.length);
// Add a node const addNode = () => { system.flowStore.getState().addNode({ id: 'new-node', type: 'math/add', position: { x: 100, y: 100 }, data: {} }); };
return <div>Nodes: {nodeCount}</div>;};SelectionStore
Section titled “SelectionStore”Tracks selected nodes and edges.
interface SelectionStore { selectedIds: string[]; // Selected node/edge IDs
select(ids: string[]): void; selectAdd(id: string): void; selectRemove(id: string): void; clearSelection(): void;
isSelected(id: string): boolean;}Usage:
const NodeComponent = ({ id }: { id: string }) => { const system = useSystem();
// Only re-render when THIS node's selection changes const isSelected = useStore( system.selectionStore, (s) => s.selectedIds.includes(id) );
const handleClick = () => { system.selectionStore.getState().select([id]); };
return ( <div onClick={handleClick} className={isSelected ? 'selected' : ''} > Node {id} </div> );};VariableStore
Section titled “VariableStore”Manages graph variables.
interface VariableStore { variables: Array<{ id: string; name: string; valueTypeName: string; initialValue: any; }>;
addVariable(variable: Variable): void; removeVariable(id: string): void; updateVariable(id: string, changes: Partial<Variable>): void; getVariableByName(name: string): Variable | undefined;}Usage:
const VariablesPanel = () => { const system = useSystem(); const variables = useStore(system.variableStore, (s) => s.variables);
const addVariable = () => { system.variableStore.getState().addVariable({ id: generateId(), name: 'newVar', valueTypeName: 'number', initialValue: 0 }); };
return ( <div> <button onClick={addVariable}>Add Variable</button> <ul> {variables.map(v => ( <li key={v.id}>{v.name}: {v.valueTypeName}</li> ))} </ul> </div> );};TabStore
Section titled “TabStore”Manages the panel layout system using rc-dock.
interface TabStore { layout: LayoutBase; // rc-dock layout structure currentPanel: string | null;
setLayout(layout: LayoutBase): void; setCurrentPanel(panelId: string): void;
openPanel(panelId: string, config?: PanelConfig): void; closePanel(panelId: string): void;}Usage:
const LayoutManager = () => { const system = useSystem(); const layout = useStore(system.tabStore, (s) => s.layout);
const openInspector = () => { const newLayout = addFloatingTab(layout, { id: 'inspector', title: 'Inspector', content: () => <InspectorPanel /> }); system.tabStore.getState().setLayout(newLayout); };
return <button onClick={openInspector}>Open Inspector</button>;};ActionStore
Section titled “ActionStore”Defines available actions and their state.
interface ActionStore { actions: { save(): void; load(): void; export(): void; clear(): void; // ... };
registerAction(name: string, handler: () => void): void; executeAction(name: string): void;}Usage:
const SaveButton = () => { const system = useSystem();
const handleSave = () => { system.actionStore.getState().actions.save(); };
return <button onClick={handleSave}>Save</button>;};Specialized Stores
Section titled “Specialized Stores”ControlStore
Section titled “ControlStore”Manages custom input controls for value types.
interface ControlStore { controls: Map<string, React.ComponentType>;
registerControl(valueType: string, component: React.ComponentType): void; getControl(valueType: string): React.ComponentType | undefined;}RegistryStore
Section titled “RegistryStore”Holds node and value type metadata.
interface RegistryStore { nodes: Map<string, NodeDefinition>; valueTypes: Map<string, ValueTypeDefinition>;
registerNode(definition: NodeDefinition): void; registerValueType(definition: ValueTypeDefinition): void;
getNode(type: string): NodeDefinition | undefined; getNodesInCategory(category: string): NodeDefinition[];}EventsStore
Section titled “EventsStore”Manages custom graph events.
interface EventsStore { events: Array<{ id: string; name: string; }>;
addEvent(event: CustomEvent): void; removeEvent(id: string): void; getEventByName(name: string): CustomEvent | undefined;}LogsStore
Section titled “LogsStore”Stores debug logs and console output.
interface LogsStore { logs: Array<{ timestamp: number; level: 'info' | 'warn' | 'error'; message: string; }>;
addLog(log: LogEntry): void; clearLogs(): void;}Store Factory Pattern
Section titled “Store Factory Pattern”All stores use a factory pattern for creation:
export const myStoreFactory = (system: System): StoreApi<MyStore> => { return createStore<MyStore>()((set, get) => ({ // Initial state someValue: 0, items: [],
// Actions increment: () => { set((state) => ({ someValue: state.someValue + 1 })); },
addItem: (item) => { set((state) => ({ items: [...state.items, item] }));
// Publish event system.pubsub.publish('item:added', item); },
// Computed values getItemById: (id) => { return get().items.find(item => item.id === id); } }));};Best Practices
Section titled “Best Practices”1. Selective Subscriptions
Section titled “1. Selective Subscriptions”Subscribe only to the data you need:
// ✅ Good - only re-renders when count changesconst count = useStore(system.flowStore, (s) => s.nodes.length);
// ❌ Bad - re-renders on any flowStore changeconst flowStore = useStore(system.flowStore);const count = flowStore.nodes.length;2. Derived State
Section titled “2. Derived State”Compute derived data in selectors:
// ✅ Good - computed in selectorconst selectedNodes = useStore(system.flowStore, (s) => { const selectedIds = system.selectionStore.getState().selectedIds; return s.nodes.filter(n => selectedIds.includes(n.id));});
// ❌ Bad - computed in component (re-runs on every render)const allNodes = useStore(system.flowStore, (s) => s.nodes);const selectedIds = useStore(system.selectionStore, (s) => s.selectedIds);const selectedNodes = allNodes.filter(n => selectedIds.includes(n.id));3. Batch Updates
Section titled “3. Batch Updates”Use Zustand’s batching for multiple updates:
system.flowStore.setState((state) => ({ ...state, nodes: [...state.nodes, newNode], edges: [...state.edges, newEdge]}));// Only triggers one re-render4. Shallow Equality
Section titled “4. Shallow Equality”Use shallow equality for object/array selectors:
import { shallow } from 'zustand/shallow';
const selectedIds = useStore( system.selectionStore, (s) => s.selectedIds, shallow // Prevent re-render if array contents haven't changed);5. Actions over Direct Mutation
Section titled “5. Actions over Direct Mutation”Use store actions instead of direct setState:
// ✅ Good - encapsulated logicsystem.flowStore.getState().addNode(node);
// ❌ Bad - bypasses event system and validationsystem.flowStore.setState((s) => ({ nodes: [...s.nodes, node]}));Integration with Events
Section titled “Integration with Events”Stores publish events when state changes:
export const flowStoreFactory = (system: System) => { return createStore<FlowStore>()((set, get) => ({ nodes: [],
addNode: (node) => { set((s) => ({ nodes: [...s.nodes, node] })); system.pubsub.publish('node:added', node); // Event published system.undoManager.pushState(); // Undo tracking },
removeNode: (nodeId) => { const node = get().nodes.find(n => n.id === nodeId); set((s) => ({ nodes: s.nodes.filter(n => n.id !== nodeId) }));
if (node) { system.pubsub.publish('node:removed', node);
// Clean up related edges const edges = get().edges.filter( e => e.source === nodeId || e.target === nodeId ); edges.forEach(edge => { system.pubsub.publish('edge:removed', edge); }); } } }));};Testing Stores
Section titled “Testing Stores”Stores can be tested in isolation:
import { describe, it, expect } from 'vitest';import { flowStoreFactory } from './flow';
describe('FlowStore', () => { it('adds nodes correctly', () => { const mockSystem = { pubsub: { publish: vi.fn() } } as any;
const store = flowStoreFactory(mockSystem);
store.getState().addNode({ id: 'test', type: 'math/add', position: { x: 0, y: 0 } });
expect(store.getState().nodes).toHaveLength(1); expect(mockSystem.pubsub.publish).toHaveBeenCalledWith( 'node:added', expect.objectContaining({ id: 'test' }) ); });});Middleware
Section titled “Middleware”Zustand supports middleware for cross-cutting concerns:
import { devtools, persist } from 'zustand/middleware';
export const persistentStoreFactory = () => { return createStore<MyStore>()( devtools( persist( (set, get) => ({ // Store implementation }), { name: 'behave-graph-storage', partialize: (state) => ({ // Only persist certain fields savedGraphs: state.savedGraphs }) } ) ) );};Performance Optimization
Section titled “Performance Optimization”Memoization
Section titled “Memoization”Use React.memo and useMemo with Zustand:
const NodeComponent = React.memo(({ id }: { id: string }) => { const system = useSystem();
const node = useStore( system.flowStore, useCallback((s) => s.nodes.find(n => n.id === id), [id]) );
return <div>{node?.type}</div>;});Computed Stores
Section titled “Computed Stores”Create stores that derive from other stores:
const computedStoreFactory = (system: System) => { return createStore<ComputedStore>()((set) => { // Subscribe to base store system.flowStore.subscribe((flowState) => { set({ nodeCount: flowState.nodes.length, edgeCount: flowState.edges.length }); });
return { nodeCount: 0, edgeCount: 0 }; });};Next Steps
Section titled “Next Steps”- Architecture Overview - Understanding the System
- Event System - Pub/sub communication
- Plugin Development - Extending stores
- Performance Guide - Optimization techniques