@statelyai/flow-react
v0.9.7
Published
Thin React adapter for @statelyai/flow with core and DOM re-exports.
Downloads
1,516
Maintainers
Readme
@statelyai/flow-react
Thin React adapter for @statelyai/flow with core and DOM re-exports.
Philosophy
@statelyai/flow-react should be completely event-driven and composable. React should not hide a second flow model behind component internals; it should adapt the same FlowInstance that consumers can create, inspect, subscribe to, and trigger directly. It re-exports the lower layers so React consumers can install and import from one package.
The adapter abstracts over two lower layers:
@statelyai/flowowns the graph, viewport, selection, interaction state, semantic events, and derived engine queries.@statelyai/flow-domowns browser-only measurement and container attachment.
The React package owns React lifecycle, context, selector subscriptions, and a convenience renderer. It should be flexible enough for consumers to build their own stateful flow component from the instance alone.
Optimization is part of the architecture, not a later cleanup pass. Components subscribe only to durable content and interaction state. During drag and animated layout sessions, @statelyai/flow-dom batches node rectangles, default group child-area and header rectangles, edge paths, endpoint handles, label positions, and multi-selection bounds into imperative animation-frame writes, so React commits only at transient-session boundaries. Default group headers keep projected styles on a separate element from imperative geometry, preserving projected transforms during drag. Default edge paths expose data-flow-edge-segment; custom edge renderers can use the same indexed attribute to join the imperative path updates. Existing custom paths whose d exactly matches a routed segment are also bound automatically, including duplicate hit strokes; unrelated decoration paths are left alone. Router-provided PathData.svgPath geometry is honored by both initial React rendering and those imperative updates.
The graph data structure is the source of truth for graph content, not for transient UI state. Core node and edge data should not grow flags like selected: true or dragging: true; selection, focus, dragging, hover, connection state, and similar interaction state live in the flow store and are derived into render props such as selected or dragging.
DOM measurement is one-directional. Nodes, ports, and edge labels report their rendered size to the engine. For autoSize nodes and label.autoSize edge labels, the adapter leaves DOM width/height content-driven and Flow keeps measurements as derived session geometry; it never writes them into authored dimensions. Explicitly resizing an autoSize node switches its wrapper to the committed dimensions while preserving the content measurement as derived data. Graph-owned entities retain their existing controlled-size behavior.
Core API
| Export | Description |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| createFlowInstance(options) | Creates the shared instance used by React and custom renderers |
| useFlow(options) | TipTap-style hook for creating a stable flow controller |
| useCreateFlowInstance(options) | Deprecated alias for useFlow(options) |
| useFlowSelector(selector, options?) | Subscribe to any part of the flow store from React |
| useFlowValue(selector, isEqual?) | Short selector hook for the nearest provider |
| useFlowContext() | Subscribe to the full flow store context |
| useConnection() | Subscribe to the active connection gesture state |
| useFlowConnections(query) | Subscribe to stable, port-aware connections for one node |
| useFlowView() | Access view-owned image export and measurement refresh |
| useKeyPress(keys) | Track whether one or more keys are currently pressed |
| useNodesInitialized() | Check whether visible auto-size nodes have measurements |
| useFlowNode(id, selector?, isEqual?) | Subscribe to node data plus derived render fields |
| useFlowEdge(id, selector?, isEqual?) | Subscribe to edge data plus derived render fields |
| useFlowPort(id, selector?, isEqual?) | Subscribe to port data plus derived render fields |
| useFlowViewport() | Subscribe to viewport state |
| useFlowMeasurement(id) | Subscribe to measured bounds for an entity ref |
| FlowInstanceProvider | Provides a FlowInstance through React context |
| useFlowInstance() | Reads the nearest FlowInstance |
| useOptionalFlowInstance() | Reads the nearest FlowInstance or returns null |
| StatelyFlow | Convenience component that renders nodes, edges, backgrounds, projections, and overlays |
| createEntityGeometryObserver(store) | Batches transient node/label rectangles, multi-entity bounds, and edge paths into imperative animation-frame DOM writes |
| PanePortal / ViewportPortal | Render custom content into fixed screen-coordinate or transformed canvas-coordinate layers |
| useFitViewExclusion() | Register an overlay element whose measured edge band fit and center commands avoid |
| EntityAnchor | Position custom controls on entity sides or free-standing canvas bounds with screen-stable offsets and inverse scaling |
| Port | Render a measured connection port or a graph-owned readonly inspection port |
| RenderEdgeLabelProps | Edge, path, bounds, interaction state, and projected attrs passed to renderEdgeLabel |
| Panel | Positioned overlay panel for controls and custom UI |
| ReactActiveGraphProjection | React-typed active projection for <StatelyFlow projections={...} /> or controller-owned projections |
| ReactGraphNodeAttrs / ReactGraphEdgeAttrs | React-typed visual attrs exposed to custom node/edge renderers |
| FlowColorModeScope | Applies the current flow color mode as DOM data attributes and color-scheme |
| useFlowColorMode() | Reads the store color mode and resolves system to light or dark |
| useApplyFlowColorMode(element) | Applies flow color mode attributes to an existing DOM element from inside a flow |
| FLOW_BASE_CSS / useFlowBaseStyles() | Base data-attribute stylesheet injected by StatelyFlow |
@statelyai/flow-react and @statelyai/flow-react/backgrounds export Background, DotsBackground, LinesBackground, and CrossBackground helpers for viewport-aware canvas backgrounds. Backgrounds are plain React components, so consumers can also build custom ones with useFlowSelector((context) => context.viewport).
Color mode is store data, not engine behavior. @statelyai/flow-dom exports small helpers for resolving and applying light, dark, and system, while React exposes hooks/components that make those values easy to use with CSS variables.
initialGraph seeds a graph that the adapter-owned flow instance owns. For caller ownership, create a controller and pass flow={flow}. There is no controlled graph prop; observe flow.on('commit', ...) or onCommit for persistence and synchronization. Runtime settings, inputModes, and projections update the existing flow instance. If projections is omitted, StatelyFlow subscribes to the instance's flow.projections lifecycle.
renderEdgeLabel replaces the built-in label content while retaining Flow's positioned, measured, interactive label wrapper. Set edge.label.autoSize: true for content-driven custom labels; the wrapper then leaves width/height unset while its measurement drives stable routed geometry. Render props include the edge's current pathData: the same EdgePathData object already routed by StatelyFlow, including both source-to-label and label-to-target segments. Transient drag geometry remains on Flow's imperative DOM path, and the renderer receives current committed geometry at gesture boundaries. Use layers.viewport for canvas-coordinate interaction content and layers.pane for screen-coordinate content. Descendants can target the same named layers with ViewportPortal and PanePortal. Flow enforces paint bands (edges → entities → labels → interaction → guides → chrome) so entity-local z-index cannot cover labels, selection chrome, anchors, or portaled toolbars. Override the namespaced --flow-layer-* variables only when intentionally changing that contract. Layer wrappers ignore pointer input; opt individual controls in with pointerEvents: "auto" and mark controls inside labels or canvas layers with data-flow-interactive when Flow gestures should ignore them.
Attach the ref from useFitViewExclusion({ side, gap }) to floating controls that cover the canvas. The hook measures the element relative to the Flow root at command time, including content portaled outside the root, so responsive toolbars and panel-relative chrome do not need guessed fit padding.
Set showSelectionBounds to render a zoom-stable, square frame around established multi-selections. Its geometry follows drag and layout frames through Flow DOM without React commits; theme [data-flow-selection-bounds], --flow-selection-color, or the screen-pixel --flow-selection-bounds-outset. Set preserveViewportCenterOnResize when root container resizes should retain the world point at its center.
The default edge renderer honors edge.style.markerStart and markerEnd values arrow, arrow-closed, circle, and none, plus safe local custom marker IDs or url(#id) references. External and malformed marker URLs are rejected. Built-in marker definitions are namespaced per StatelyFlow, so multiple mounted flows cannot collide. It also renders canonical graph edge.points/routing and optional edge.svgPath without rerouting or splitting the route around a label.
Render layout-owned ports with <Port measured={false} interaction="inspect" />. Inspection ports are focusable, selectable addressables (${node.id}:${port.name}), emit the normal entity click events, and never start a connection gesture. Omit interaction (or use "connect") to retain existing connection behavior.
Custom node and edge renderers should branch on the application-defined entity.kind. Keep entity.type as the structural "node" or "edge" discriminant so core selection, drag, connection, and CRUD behavior remains valid.
For controlled reconnect gestures, reconnectMode({ commit: false, preview: false }) keeps Flow's input lifecycle while suppressing the adapter's projected preview edge, so an external domain store can render its own temporary geometry without duplicate paths.
EntityAnchor can be nested inside a node or edge-label renderer with side, a child anchor point, cross-axis align, and an outward offset. The nested anchor follows the parent entity's imperative drag geometry without React commits. With inverseScale, its size and offset remain constant in screen pixels and its transform origin matches the selected child anchor. The existing bounds plus AnchorSpec form remains available for free-standing canvas-space overlays.
StatelyFlow exposes stable, namespaced selector roles while keeping data-flow-* attributes authoritative: .stately-flow / .stately-flow__pane (interactive root), .stately-flow__viewport (transformed canvas), .stately-flow__edges, .stately-flow__nodes, .stately-flow__edge-labels, .stately-flow__selection-bounds, their singular entity classes, .stately-flow__background, .stately-flow__viewport-layer, .stately-flow__pane-layer, and .stately-flow__overlays. Consumer className values are merged with these roles. Focus presentation is consumer-owned and visually off by default: set --flow-focus-outline or target :focus-visible for browser focus, and use [data-flow-focused="true"] for synchronized Flow focus state.
Use rootRef and rootProps to access that input-owned root or attach ordinary div attributes and events such as context menu, double-click, and drag/drop. rootProps.className and rootProps.style merge with the top-level values; Flow's input runtime remains the sole gesture owner.
Keyboard handling is scoped to the StatelyFlow root by default so multiple flows on one page do not all receive Delete/undo/arrow keys. Pass keyboardTarget="window" to restore global handling. Pass settings={{ keymap }} to rebind built-in shortcuts. Double-click zoom is enabled in the default input modes; Shift double-click zooms out.
Alignment and equal-spacing guides stay a constant screen size across zoom and expose --flow-snap-line-color and --flow-spacing-guide-color for theming. Equal-spacing guides include their rounded canvas distance.
Usage
import {
createEdge,
createGraph,
createNode,
StatelyFlow,
useFlow,
useFlowSelector,
} from '@statelyai/flow-react';
const graph = createGraph({
nodes: [
createNode({ id: 'a', x: 0, y: 0, data: { label: 'Start' } }),
createNode({ id: 'b', x: 240, y: 120, data: { label: 'Done' } }),
],
edges: [createEdge({ id: 'a-b', sourceId: 'a', targetId: 'b' })],
});
function Flow() {
const flow = useFlow({
initialGraph: graph,
settings: {
store: { snapToGrid: true, snapGrid: [16, 16] },
},
listeners: [
{
selector: (context) => context.selection.size,
onChange: (selectionSize) => {
console.log('selection size', selectionSize);
},
},
],
});
return (
<StatelyFlow
flow={flow}
projections={[
{
projection: { id: 'coverage', nodes: { a: 'visited' } },
mapper: {
getNodeAttrs: () => ({ color: '#16a34a', badge: 'visited' }),
},
},
]}
renderNode={({ node, selected }) => (
<div data-selected={selected}>{node.data?.label ?? node.id}</div>
)}
/>
);
}Storybook
pnpm --filter @statelyai/flow-react storybook