Learn / React in 10 minutes
React in 10 minutes
From npm install to a flow editor with custom nodes, connection rules, undo and export. Every snippet below is real, current API — the same code the React demo gallery runs.
1 — Install and mount a canvas
npm install @grafloria/react @grafloria/element @grafloria/renderer @grafloria/engine react react-dom
One component does the heavy lifting: <GrafloriaFlow>. Give it nodes,
edges, and — this matters — a parent with a real height. The canvas fills
100% of its container, and 100% of zero is an empty page.
import { GrafloriaFlow } from '@grafloria/react';
const nodes = [
{ id: 'a', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
{ id: 'b', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];
export default function App() {
return (
<div style={{ height: '100vh' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} plugins />
</div>
);
}
That's already a working editor: drag nodes, draw connections between them, pan and
zoom, and — because of plugins — a minimap, zoom controls and a dotted
background (all lazy-loaded, they cost nothing if you omit the prop).
defaultNodes/defaultEdges is the uncontrolled form: the
canvas owns the data from here on. No stylesheet import is needed — the renderer injects
its own scoped styles.
2 — Controlled state, the React way
When your app needs to own the graph, use useNodesState /
useEdgesState. Each returns a triple — and the third element is the
change handler for the canvas, not the second:
import { GrafloriaFlow, useNodesState, useEdgesState } from '@grafloria/react';
function Editor() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<div style={{ height: '100vh' }}>
<button onClick={() => setNodes((all) => [...all,
{ id: 'n' + all.length, position: { x: 80, y: 80 }, data: { label: 'New' } }])}>
Add node
</button>
<GrafloriaFlow
nodes={nodes} onNodesChange={onNodesChange}
edges={edges} onEdgesChange={onEdgesChange} />
</div>
);
}
setNodes updates your state with
specs; onNodesChange receives the engine's live NodeModel[]
after a drag or delete and folds them back into your state. Wiring
setNodes into onNodesChange type-errors — the tuple keeps the
two directions separate on purpose.3 — Custom nodes are just components
Map a node type to a component with nodeTypes, and mark the
spec custom: true so it renders through the HTML layer instead of as SVG.
The node still drags, routes and hit-tests like any other:
function Card({ data, selected }) {
return (
<div style={{ height: '100%', background: '#fff', borderRadius: 12,
border: selected ? '2px solid #3B52D9' : '1.5px solid #94A5F0',
padding: '10px 14px', boxSizing: 'border-box' }}>
<div style={{ fontWeight: 700 }}>{data.title}</div>
<div style={{ fontSize: 12, color: '#5A6478' }}>owner: {data.owner}</div>
</div>
);
}
const nodes = [
{ id: 'a', type: 'card', custom: true, position: { x: 80, y: 90 },
size: { width: 230, height: 110 }, data: { title: 'Build', owner: 'CI' } },
{ id: 'b', type: 'card', custom: true, position: { x: 430, y: 90 },
size: { width: 230, height: 110 }, data: { title: 'Deploy', owner: 'CD' } },
];
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} nodeTypes={{ card: Card }} />
Your component receives { id, data, selected, node } — node
is the live engine model, the escape hatch when you need more than data.
4 — Ports and connection rules
Ports are part of the node spec — no separate handle components. Declare them with a
side, a direction, and optionally a dataType:
const nodes = [
{ id: 'src', position: { x: 120, y: 260 }, size: { width: 130, height: 70 }, label: 'number src',
ports: [{ id: 'out', side: 'right', type: 'output', dataType: 'number' }] },
{ id: 'num', position: { x: 640, y: 140 }, size: { width: 130, height: 70 }, label: 'number in',
ports: [{ id: 'nin', side: 'left', type: 'input', dataType: 'number' }] },
{ id: 'str', position: { x: 640, y: 400 }, size: { width: 130, height: 70 }, label: 'string in',
ports: [{ id: 'sin', side: 'left', type: 'input', dataType: 'string' }] },
];
Two validation mechanisms compose. Type compatibility is declarative — register the types once and incompatible ports refuse the wire mid-drag:
import { portTypeRegistry } from '@grafloria/element';
portTypeRegistry.registerAll([
{ name: 'number', color: '#2563eb', compatibleWith: ['number'] },
{ name: 'string', color: '#9333ea', compatibleWith: ['string'] },
]);
Custom rules are a validator function — return true to allow, or a
string to veto with a reason:
import { registerConnectionValidator, clearConnectionValidators } from '@grafloria/element';
useEffect(() => {
const dispose = registerConnectionValidator(({ sourcePort, targetPort }) => {
if (sourcePort?.type === 'output' && targetPort?.type === 'output')
return 'an output cannot feed another output';
return true;
});
return () => { dispose(); clearConnectionValidators(); };
}, []);
interaction={{ portVisibility: 'always' }} on the flow.5 — Undo comes free; buttons reach the engine
⌘Z/Ctrl+Z and ⌘⇧Z/Ctrl+Y work with zero wiring — every gesture is a command on the engine's history. For your own buttons, reach the engine through the instance:
import type { DiagramInstance } from '@grafloria/react';
const inst = useRef<DiagramInstance | null>(null);
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
onInit={(instance) => { inst.current = instance; }} />
<button onClick={() => void inst.current?.getEngine().undo()}>Undo</button>
<button onClick={() => void inst.current?.getEngine().redo()}>Redo</button>
6 — Auto-layout
Stop hand-placing nodes: name a layout and the engine arranges them. ELK is the heavyweight — it loads lazily (a separate ~432 KB gz chunk) only when first used:
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} layout="elk" />
// or configured:
<GrafloriaFlow layout={{ name: 'dagre', options: { direction: 'TB', rankSpacing: 80 } }} ... />
Registered names: auto, elk, dagre,
layered, tree, grid, circular,
radial, force, spectral, community.
The prop re-runs on value change only — it will never fight a user's drag. To
re-layout on demand, call instance.getEngine().layout('layered', { direction: 'LR' }).
7 — Save, load, export
// PNG (data: URL) and SVG (raw string) — exports the scene graph, not a screenshot
const png = await inst.current.export('png', { scale: 2 });
const svg = await inst.current.export('svg');
const pdf = await inst.current.export('pdf'); // real vector PDF
// Mermaid-compatible text round-trip
const text = inst.current.exportText(); // valid Mermaid + position sidecar
inst.current.loadText(text); // reconciles into the live canvas
Pass { embedModel: true } to PNG/SVG export and the diagram model rides
inside the file — the exported image re-opens as an editable diagram.
Where next
- Every demo as a React component — 100+ routes, source shown.
- The model — what nodes, ports, links and groups really are.
- Collaboration — the
collabprop and CRDT sync. - Next.js: all components carry
'use client'; for server rendering use thessrprop withrenderToStaticSVG()— nodynamic()tricks needed.