Comprehensive React Flow (@xyflow/react) patterns and best practices for building node-based UIs, workflow editors, and interactive diagrams. Use when working with React Flow for (1) building flow editors or node-based interfaces, (2) creating custom nodes and edges, (3) implementing drag-and-drop workflows, (4) optimizing performance for large graphs, (5) managing flow state and interactions, (6) implementing auto-layout or positioning, or (7) TypeScript integration with React Flow.
95
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Use the toObject() method from useReactFlow() to serialize the complete flow state (nodes, edges, viewport) for saving and restoring.
Incorrect (manual serialization):
// ❌ Manually collecting state - misses internal React Flow state
const saveFlow = () => {
const flow = {
nodes,
edges,
// Missing: viewport, internal positions, connection state
};
localStorage.setItem('flow', JSON.stringify(flow));
};
// Missing viewport, zoom level, pan positionCorrect (using toObject):
import { useCallback } from 'react';
import { useReactFlow } from '@xyflow/react';
function SaveRestore() {
const { toObject, setNodes, setEdges, setViewport } = useReactFlow();
// ✅ Save complete flow state
const onSave = useCallback(() => {
const flow = toObject();
localStorage.setItem('flow', JSON.stringify(flow));
console.log('Saved:', flow);
}, [toObject]);
// ✅ Restore complete flow state
const onRestore = useCallback(() => {
const flowStr = localStorage.getItem('flow');
if (!flowStr) return;
const flow = JSON.parse(flowStr);
if (flow) {
setNodes(flow.nodes || []);
setEdges(flow.edges || []);
setViewport(flow.viewport || { x: 0, y: 0, zoom: 1 });
}
}, [setNodes, setEdges, setViewport]);
return (
<>
<button onClick={onSave}>Save</button>
<button onClick={onRestore}>Restore</button>
</>
);
}Save to Server:
const saveToServer = useCallback(async () => {
const flow = toObject();
try {
await fetch('/api/flows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: flowId,
data: flow,
updatedAt: new Date().toISOString(),
}),
});
} catch (error) {
console.error('Save failed:', error);
}
}, [toObject, flowId]);
const loadFromServer = useCallback(async () => {
try {
const response = await fetch(`/api/flows/${flowId}`);
const { data } = await response.json();
setNodes(data.nodes || []);
setEdges(data.edges || []);
setViewport(data.viewport || { x: 0, y: 0, zoom: 1 });
} catch (error) {
console.error('Load failed:', error);
}
}, [flowId, setNodes, setEdges, setViewport]);Export as JSON:
const exportFlow = useCallback(() => {
const flow = toObject();
const dataStr = JSON.stringify(flow, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'flow.json';
link.click();
URL.revokeObjectURL(url);
}, [toObject]);Import from JSON:
const importFlow = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const flow = JSON.parse(e.target?.result as string);
setNodes(flow.nodes || []);
setEdges(flow.edges || []);
setViewport(flow.viewport || { x: 0, y: 0, zoom: 1 });
} catch (error) {
console.error('Invalid flow file:', error);
}
};
reader.readAsText(file);
}, [setNodes, setEdges, setViewport]);toObject() Returns:
{
nodes: Node[]; // All nodes with positions and data
edges: Edge[]; // All edges with connections
viewport: { // Current viewport state
x: number;
y: number;
zoom: number;
};
}Additional Context:
Reference: Save and Restore Example