This document consolidates instructions from all installed React Flow skills. Use the appropriate sections based on your task.
This project uses React Flow v12+ (@xyflow/react) for building interactive node-based UIs. The following skills provide comprehensive guidance:
- react-flow — Core implementation & fundamentals
- react-flow-architecture — Design patterns & state management
- react-flow-advanced — Advanced patterns (undo/redo, sub-flows, etc.)
- react-flow-implementation — Practical implementation examples
- react-flow-code-review — Anti-patterns & performance issues
Follow these rules when writing React Flow code:
- Always import from
@xyflow/react— never from legacyreactfloworreact-flow-rendererpackages. - Always import the stylesheet:
import '@xyflow/react/dist/style.css'(orbase.cssfor custom styling frameworks). - The
<ReactFlow>parent container MUST have explicit width and height — this is the #1 cause of blank canvases. - Define
nodeTypesandedgeTypesobjects OUTSIDE component bodies or wrap inuseMemoto prevent re-renders. - Prefer custom nodes over built-in nodes — the React Flow team explicitly recommends this.
- Use the
nodragclass on interactive elements inside custom nodes (inputs, buttons, selects). - Use
nowheelclass on scrollable elements inside custom nodes to prevent zoom interference. - When hiding handles, use
visibility: hiddenoropacity: 0— neverdisplay: none(breaks dimension calculation). - When using multiple handles of the same type on a node, always assign unique
idprops. - After programmatically adding/removing handles, call
useUpdateNodeInternalsto refresh the node. - Always create new objects when updating node/edge state — mutations are not detected by React Flow.
- Prefer controlled flows (with
onNodesChange/onEdgesChange/onConnect) for any non-trivial application.
When starting a React Flow task:
- Clarify the goal: new flow setup, custom nodes/edges, state management, layout, performance, styling, E2E testing, advanced patterns (undo/redo, copy/paste, computed flows, collaboration), or debugging.
- Collect minimal facts:
- React Flow version (v12+ uses
@xyflow/react) - TypeScript or JavaScript
- State management approach (local state, Zustand, Redux)
- Number of nodes expected (affects performance strategy)
- Styling approach (CSS, Tailwind, styled-components)
- React Flow version (v12+ uses
- Branch quickly:
- Migrating from legacy
reactflow→ package rename, import changes, API differences - Blank canvas or missing nodes → container dimensions or missing CSS import
- Edges not rendering → missing handles, missing CSS, or
display: noneon handles - Re-renders or sluggish performance → nodeTypes defined inside component, missing memoization
- Connecting nodes not working → missing
onConnecthandler or handle configuration - Layout/positioning → external layout library integration (dagre, elkjs)
- Type errors → TypeScript generic patterns for Node/Edge types
- Migrating from legacy
| Issue | Cause | Solution |
|---|---|---|
| Blank canvas with no errors | Parent container has no height | Set explicit height: 100vh or equivalent |
nodeTypes / edgeTypes warning |
Object recreated each render | Move outside component body or wrap in useMemo |
| Edges render but in wrong position | Handles use display: none |
Switch to opacity: 0 or visibility: hidden |
| Cannot interact with inputs inside nodes | Missing nodrag class |
Add className="nodrag" to interactive elements |
| Nodes snap back after drag | onNodesChange not wired up |
Ensure handler applies changes correctly |
| Connection line appears but edge never creates | onConnect handler missing |
Add onConnect handler and call addEdge |
| Multiple handles on same side overlap | Not positioned with CSS | Use CSS (top offset) and assign unique ids |
| State updates don't reflect in nodes | Creating mutations | Use spread operator to create new objects |
| Zustand context warning | Two versions of @xyflow/react or missing provider |
Check install, add <ReactFlowProvider> |
| Sub-flow child nodes render behind parent | Ordering issue | Ensure parent nodes appear before children in array |
Before considering a feature complete:
- Confirm
@xyflow/react/dist/style.cssis imported (orbase.css+ custom styles) - Confirm parent container has explicit width and height
- Confirm
nodeTypes/edgeTypesare stable references (defined outside component or memoized) - Confirm custom nodes use
<Handle>components with propertypeandposition - Confirm interactive elements inside nodes have
nodragclass - Confirm controlled flows wire up all three handlers:
onNodesChange,onEdgesChange,onConnect - Confirm state updates create new node/edge objects (no mutations)
- Confirm TypeScript generics are applied to hooks and callbacks for type safety
- Confirm performance-sensitive flows memoize custom node/edge components with
React.memo
- Visual programming interfaces
- Workflow builders and automation tools
- Diagram editors (flowcharts, org charts)
- Data pipeline visualization
- Mind mapping tools
- Node-based audio/video editors
- Decision tree builders
- State machine designers
- Simple static diagrams → use SVG or canvas directly
- Heavy real-time collaboration → may need custom sync layer
- 3D visualizations → use Three.js, react-three-fiber
- Graph analysis with 10k+ nodes → use WebGL-based solutions like Sigma.js
@xyflow/system (vanilla TypeScript)
├── Core algorithms (edge paths, bounds, viewport)
├── xypanzoom (d3-based pan/zoom)
├── xydrag, xyhandle, xyminimap, xyresizer
└── Shared types
@xyflow/react (depends on @xyflow/system)
├── React components and hooks
├── Zustand store for state management
└── Framework-specific integrations
@xyflow/svelte (depends on @xyflow/system)
└── Svelte components and stores
Implication: Core logic is framework-agnostic. When debugging, check if issue is in @xyflow/system or framework-specific package.
// useNodesState/useEdgesState for prototyping
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);Pros: Simple, minimal boilerplate Cons: State isolated to component tree
import { create } from 'zustand';
interface FlowStore {
nodes: Node[];
edges: Edge[];
setNodes: (nodes: Node[]) => void;
onNodesChange: OnNodesChange;
}
const useFlowStore = create<FlowStore>((set, get) => ({
nodes: initialNodes,
edges: initialEdges,
setNodes: (nodes) => set({ nodes }),
onNodesChange: (changes) => {
set({ nodes: applyNodeChanges(changes, get().nodes) });
},
}));
// In component
function Flow() {
const { nodes, edges, onNodesChange } = useFlowStore();
return <ReactFlow nodes={nodes} onNodesChange={onNodesChange} />;
}Pros: State accessible anywhere, easier persistence/sync Cons: More setup, need careful selector optimization
const nodes = useSelector(selectNodes);
const dispatch = useDispatch();
const onNodesChange = useCallback((changes: NodeChange[]) => {
dispatch(nodesChanged(changes));
}, [dispatch]);User Input → Change Event → Reducer/Handler → State Update → Re-render
↓
[Drag node] → onNodesChange → applyNodeChanges → setNodes → ReactFlow
↓
[Connect] → onConnect → addEdge → setEdges → ReactFlow
↓
[Delete] → onNodesDelete → deleteElements → setNodes/setEdges → ReactFlow
const nodes = [
{
id: 'group-1',
type: 'group',
position: { x: 0, y: 0 },
style: { width: 300, height: 200 },
},
{
id: 'child-1',
parentId: 'group-1', // Key: parent reference
extent: 'parent', // Key: constrain to parent
position: { x: 10, y: 30 }, // Relative to parent
data: { label: 'Child' },
},
];Considerations:
- Use
extent: 'parent'to constrain dragging - Use
expandParent: trueto auto-expand parent - Parent z-index affects child rendering order
const { toObject, setViewport } = useReactFlow();
const handleSave = () => {
const flow = toObject();
// flow.nodes, flow.edges, flow.viewport
localStorage.setItem('flow', JSON.stringify(flow));
};
const handleRestore = () => {
const flow = JSON.parse(localStorage.getItem('flow'));
setNodes(flow.nodes);
setEdges(flow.edges);
setViewport(flow.viewport);
};| Node Count | Strategy |
|---|---|
| < 100 | Default settings |
| 100-500 | Enable onlyRenderVisibleElements |
| 500-1000 | Simplify custom nodes, reduce DOM elements |
| > 1000 | Consider virtualization, WebGL alternatives |
<ReactFlow
// Only render nodes/edges in viewport
onlyRenderVisibleElements={true}
// Reduce node border radius (improves intersect calculations)
nodeExtent={[[-1000, -1000], [1000, 1000]]}
// Disable features not needed
elementsSelectable={false}
panOnDrag={false}
zoomOnScroll={false}
/>See Architecture section above. Key additions:
function GroupNode({ data, id }: NodeProps) {
return (
<div className="group-node">
<div className="group-header">{data.label}</div>
{/* Children are rendered automatically by React Flow */}
</div>
);
}import { ConnectionLineComponentProps, getSmoothStepPath } from '@xyflow/react';
function CustomConnectionLine({
fromX, fromY, fromPosition,
toX, toY, toPosition,
connectionStatus,
}: ConnectionLineComponentProps) {
const [path] = getSmoothStepPath({
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
});
return (
<g>
<path
d={path}
fill="none"
stroke={connectionStatus === 'valid' ? '#22c55e' : '#ef4444'}
strokeWidth={2}
strokeDasharray="5 5"
/>
</g>
);
}
<ReactFlow connectionLineComponent={CustomConnectionLine} />import { useReactFlow, useCallback, useRef } from 'react';
function DnDFlow() {
const reactFlowWrapper = useRef(null);
const { screenToFlowPosition, addNodes } = useReactFlow();
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback((event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
if (!type) return;
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode = {
id: `${Date.now()}`,
type,
position,
data: { label: `${type} node` },
};
addNodes(newNode);
}, [screenToFlowPosition, addNodes]);
return (
<div ref={reactFlowWrapper} style={{ height: '100%' }}>
<ReactFlow
onDragOver={onDragOver}
onDrop={onDrop}
onInit={setReactFlowInstance}
/>
</div>
);
}
// Sidebar component
function Sidebar() {
const onDragStart = (event: DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<aside>
<div draggable onDragStart={(e) => onDragStart(e, 'input')}>
Input Node
</div>
<div draggable onDragStart={(e) => onDragStart(e, 'default')}>
Default Node
</div>
</aside>
);
}import { useCallback, useState } from 'react';
function useUndoRedo<T>(initialState: T) {
const [history, setHistory] = useState<T[]>([initialState]);
const [index, setIndex] = useState(0);
const state = history[index];
const setState = useCallback((newState: T | ((prev: T) => T)) => {
setHistory((prev) => {
const resolved = typeof newState === 'function'
? (newState as (prev: T) => T)(prev[index])
: newState;
const newHistory = prev.slice(0, index + 1);
return [...newHistory, resolved];
});
setIndex((i) => i + 1);
}, [index]);
const undo = useCallback(() => {
setIndex((i) => Math.max(0, i - 1));
}, []);
const redo = useCallback(() => {
setIndex((i) => Math.min(history.length - 1, i + 1));
}, [history.length]);
const canUndo = index > 0;
const canRedo = index < history.length - 1;
return { state, setState, undo, redo, canUndo, canRedo };
}import dagre from 'dagre';
interface LayoutOptions {
direction: 'TB' | 'BT' | 'LR' | 'RL';
nodeWidth: number;
nodeHeight: number;
}
function getLayoutedElements(
nodes: Node[],
edges: Edge[],
options: LayoutOptions = { direction: 'TB', nodeWidth: 172, nodeHeight: 36 }
) {
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: options.direction });
g.setDefaultEdgeLabel(() => ({}));
nodes.forEach((node) => {
g.setNode(node.id, {
width: node.measured?.width ?? options.nodeWidth,
height: node.measured?.height ?? options.nodeHeight,
});
});
edges.forEach((edge) => {
g.setEdge(edge.source, edge.target);
});
dagre.layout(g);
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = g.node(node.id);
return {
...node,
position: {
x: nodeWithPosition.x - (node.measured?.width ?? options.nodeWidth) / 2,
y: nodeWithPosition.y - (node.measured?.height ?? options.nodeHeight) / 2,
},
};
});
return { nodes: layoutedNodes, edges };
}
// Usage after nodes are measured
function Flow() {
const { fitView } = useReactFlow();
const onLayout = useCallback((direction: 'TB' | 'LR') => {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
nodes,
edges,
{ direction, nodeWidth: 150, nodeHeight: 50 }
);
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
window.requestAnimationFrame(() => {
fitView({ duration: 500 });
});
}, [nodes, edges, setNodes, setEdges, fitView]);
}function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { screenToFlowPosition } = useReactFlow();
const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
if (!connectionState.isValid && connectionState.fromHandle) {
const id = `${Date.now()}`;
const { clientX, clientY } = 'changedTouches' in event
? event.changedTouches[0]
: event;
const newNode = {
id,
position: screenToFlowPosition({ x: clientX, y: clientY }),
data: { label: 'New Node' },
};
setNodes((nds) => [...nds, newNode]);
setEdges((eds) => [
...eds,
{
id: `e-${connectionState.fromNode?.id}-${id}`,
source: connectionState.fromNode?.id ?? '',
target: id,
},
]);
}
},
[screenToFlowPosition, setNodes, setEdges]
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnectEnd={onConnectEnd}
/>
);
}import { useNodesData, type EdgeProps } from '@xyflow/react';
function DataEdge({ source, target, ...props }: EdgeProps) {
const nodesData = useNodesData([source, target]);
const sourceData = nodesData[0];
const targetData = nodesData[1];
const [path, labelX, labelY] = getSmoothStepPath(props);
return (
<>
<BaseEdge path={path} />
<EdgeLabelRenderer>
<div style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}>
{sourceData?.data?.label} → {targetData?.data?.label}
</div>
</EdgeLabelRenderer>
</>
);
}import { useKeyPress } from '@xyflow/react';
function Flow() {
const { deleteElements, getNodes, getEdges, fitView } = useReactFlow();
// Ctrl/Cmd + A: Select all
const selectAllPressed = useKeyPress(['Meta+a', 'Control+a']);
useEffect(() => {
if (selectAllPressed) {
setNodes((nds) => nds.map((n) => ({ ...n, selected: true })));
setEdges((eds) => eds.map((e) => ({ ...e, selected: true })));
}
}, [selectAllPressed]);
// Delete handler
const deletePressed = useKeyPress(['Backspace', 'Delete']);
useEffect(() => {
if (deletePressed) {
const selectedNodes = getNodes().filter((n) => n.selected);
const selectedEdges = getEdges().filter((e) => e.selected);
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
}
}, [deletePressed]);
}import { useCallback } from 'react';
import { useStore, type ReactFlowState } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
const nodesSelector = (state: ReactFlowState) => state.nodes;
const flowStateSelector = (state: ReactFlowState) => ({
nodes: state.nodes,
edges: state.edges,
viewport: state.transform,
});
function FlowInfo() {
const { nodes, edges, viewport } = useStore(flowStateSelector, shallow);
return <div>Nodes: {nodes.length}, Edges: {edges.length}</div>;
}import { ReactFlow, useNodesState, useEdgesState, addEdge } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } },
{ id: '2', position: { x: 200, y: 100 }, data: { label: 'Node 2' } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
export default function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges]
);
return (
<div style={{ width: '100%', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
/>
</div>
);
}import type { Node, Edge, NodeProps, BuiltInNode } from '@xyflow/react';
type CustomNode = Node<{ value: number; label: string }, 'custom'>;
type MyNode = CustomNode | BuiltInNode;
type MyEdge = Edge<{ weight?: number }>;
const [nodes, setNodes] = useNodesState<MyNode>(initialNodes);import { memo } from 'react';
import { Handle, Position, type NodeProps } from '@xyflow/react';
type CounterNode = Node<{ count: number }, 'counter'>;
const CounterNode = memo(function CounterNode({ data, isConnectable }: NodeProps<CounterNode>) {
return (
<>
<Handle type="target" position={Position.Top} isConnectable={isConnectable} />
<div className="counter-node">
Count: {data.count}
<button className="nodrag" onClick={() => console.log('clicked')}>
Increment
</button>
</div>
<Handle type="source" position={Position.Bottom} isConnectable={isConnectable} />
</>
);
});
const nodeTypes = { counter: CounterNode };
<ReactFlow nodeTypes={nodeTypes} ... /><Handle type="source" position={Position.Right} id="a" />
<Handle type="source" position={Position.Right} id="b" style={{ top: 20 }} />
const edge = {
id: 'e1-2',
source: '1',
sourceHandle: 'a',
target: '2',
targetHandle: null
};import { BaseEdge, EdgeProps, getSmoothStepPath } from '@xyflow/react';
function CustomEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data }: EdgeProps) {
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX, sourceY, sourcePosition,
targetX, targetY, targetPosition,
});
return (
<>
<BaseEdge id={id} path={edgePath} />
<text x={labelX} y={labelY} className="edge-label">{data?.label}</text>
</>
);
}
const edgeTypes = { custom: CustomEdge };const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
);
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/>import { useReactFlow, ReactFlowProvider } from '@xyflow/react';
function FlowControls() {
const {
getNodes, setNodes, addNodes, updateNodeData,
getEdges, setEdges, addEdges,
fitView, zoomIn, zoomOut, setViewport,
deleteElements, toObject,
} = useReactFlow();
const addNode = () => {
addNodes({ id: `${Date.now()}`, position: { x: 100, y: 100 }, data: { label: 'New' } });
};
return <button onClick={addNode}>Add Node</button>;
}
function App() {
return (
<ReactFlowProvider>
<Flow />
<FlowControls />
</ReactFlowProvider>
);
}const isValidConnection = useCallback((connection: Connection) => {
if (connection.source === connection.target) return false;
const sourceNode = getNode(connection.source);
const targetNode = getNode(connection.target);
return sourceNode?.type !== targetNode?.type;
}, []);
<ReactFlow isValidConnection={isValidConnection} /><ReactFlow
// Core data
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
// Custom types (define OUTSIDE component)
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
// Connections
onConnect={onConnect}
connectionMode={ConnectionMode.Loose}
isValidConnection={isValidConnection}
// Viewport
fitView
minZoom={0.1}
maxZoom={4}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
// Interaction
nodesDraggable={true}
nodesConnectable={true}
elementsSelectable={true}
panOnDrag={true}
zoomOnScroll={true}
// Additional components
<MiniMap />
<Controls />
<Background variant={BackgroundVariant.Dots} />
</ReactFlow>| Class | Effect |
|---|---|
nodrag |
Prevent dragging when clicking element |
nowheel |
Prevent zoom on wheel events |
nopan |
Prevent panning from element |
nokey |
Prevent keyboard events (use on inputs) |
Problem: Causes all nodes to re-mount on every render.
// BAD - recreates object every render
function Flow() {
const nodeTypes = { custom: CustomNode }; // WRONG
return <ReactFlow nodeTypes={nodeTypes} />;
}
// GOOD - defined outside component
const nodeTypes = { custom: CustomNode };
function Flow() {
return <ReactFlow nodeTypes={nodeTypes} />;
}
// GOOD - useMemo if dynamic
function Flow() {
const nodeTypes = useMemo(() => ({ custom: CustomNode }), []);
return <ReactFlow nodeTypes={nodeTypes} />;
}Problem: Custom components re-render on every parent update.
// BAD - no memoization
function CustomNode({ data }: NodeProps) {
return <div>{data.label}</div>;
}
// GOOD - wrapped in memo
import { memo } from 'react';
const CustomNode = memo(function CustomNode({ data }: NodeProps) {
return <div>{data.label}</div>;
});Problem: Creates new function references, breaking memoization.
// BAD - inline callback
<ReactFlow
onNodesChange={(changes) => setNodes(applyNodeChanges(changes, nodes))}
/>
// GOOD - memoized callback
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
<ReactFlow onNodesChange={onNodesChange} />// BAD - will throw error
function App() {
const { getNodes } = useReactFlow(); // ERROR: No provider
return <ReactFlow ... />;
}
// GOOD - wrap in provider
function FlowContent() {
const { getNodes } = useReactFlow(); // Works
return <ReactFlow ... />;
}
function App() {
return (
<ReactFlowProvider>
<FlowContent />
</ReactFlowProvider>
);
}Problem: Reference equality checks fail, causing unnecessary updates.
// BAD - new object reference every time
setNodes(nodes.map(n => ({
...n,
data: { ...n.data, config: { nested: 'value' } }
})));
// GOOD - use updateNodeData for targeted updates
const { updateNodeData } = useReactFlow();
updateNodeData(nodeId, { config: { nested: 'value' } });- Custom nodes wrapped in
memo() - nodeTypes defined outside component or memoized
- Heavy computations inside nodes use
useMemo - Event handlers use
useCallback
- Custom edges wrapped in
memo() - edgeTypes defined outside component or memoized
- Edge path calculations are not duplicated
- Using functional form of setState:
setNodes((nds) => ...) - Not spreading entire state for single property updates
- Using
updateNodeDatafor data-only changes - Batch updates when adding multiple nodes/edges
- Not calling
fitView()on every render - Using
fitViewOptionsfor initial fit only - Animation durations are reasonable (< 500ms)
// BAD - no height, flow won't render
<ReactFlow nodes={nodes} edges={edges} />
// GOOD - explicit dimensions
<div style={{ width: '100%', height: '100vh' }}>
<ReactFlow nodes={nodes} edges={edges} />
</div>// Required for default styles
import '@xyflow/react/dist/style.css';// BAD - clicking button drags node
<button onClick={handleClick}>Click</button>
// GOOD - prevents drag
<button className="nodrag" onClick={handleClick}>Click</button>// BAD - string literals
<Handle type="source" position="right" />
// GOOD - type-safe constants
import { Position } from '@xyflow/react';
<Handle type="source" position={Position.Right} />// BAD - direct mutation
nodes[0].position = { x: 100, y: 100 };
setNodes(nodes);
// GOOD - immutable update
setNodes(nodes.map(n =>
n.id === '1' ? { ...n, position: { x: 100, y: 100 } } : n
));// BAD - loses type safety
const [nodes, setNodes] = useNodesState(initialNodes);
// GOOD - explicit types
type MyNode = Node<{ value: number }, 'custom'>;
const [nodes, setNodes] = useNodesState<MyNode>(initialNodes);// BAD - using wrong type
function CustomNode(props: any) { ... }
// GOOD - correct props type
function CustomNode(props: NodeProps<MyNode>) { ... }- Are all custom components memoized?
- Are nodeTypes/edgeTypes defined outside render?
- Are callbacks wrapped in useCallback?
- Is the container sized properly?
- Are styles imported?
- Is useReactFlow used inside a provider?
- Are interactive elements marked with nodrag?
- Are types used consistently throughout?
import {
ReactFlow,
ReactFlowProvider,
useNodesState,
useEdgesState,
useReactFlow,
addEdge,
Handle,
Position,
Controls,
MiniMap,
Background,
BackgroundVariant,
applyNodeChanges,
applyEdgeChanges,
type Node,
type Edge,
type NodeProps,
type EdgeProps,
type Connection,
type NodeChange,
type EdgeChange,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';When you need specific guidance:
- Setting up a flow or custom nodes/edges → Start with "First 60 Seconds" section above
- Designing architecture or choosing state management → See "Architecture & Design Patterns"
- Building advanced features → Check "Advanced Patterns"
- Debugging or performance issues → Review "Code Review Checklist" and common pitfalls
- Implementing specific features → Find examples in "Implementation Examples"