React Flow Node Components
Patterns for building custom React Flow node components with TypeScript and Zustand.
When to Use
-
Building custom nodes for a React Flow canvas
-
Creating visual workflow / pipeline editors
-
Implementing node-based UIs with typed data
-
Adding new node types to an existing React Flow project
Quick Start
-
Copy the patterns below and replace placeholders:
-
{{NodeName}} — PascalCase component name (e.g., VideoNode )
-
{{nodeType}} — kebab-case type identifier (e.g., video-node )
-
{{NodeData}} — Data interface name (e.g., VideoNodeData )
Node Component Pattern
import { memo } from 'react'; import { Handle, Position, NodeResizer, type NodeProps } from '@xyflow/react'; import { useAppStore } from '@/store/app-store'; import type { {{NodeName}}Data } from '@/types';
type {{NodeName}}Props = NodeProps<Node<{{NodeName}}Data, '{{nodeType}}'>>;
export const {{NodeName}} = memo(function {{NodeName}}({ id, data, selected, width, height, }: {{NodeName}}Props) { const updateNode = useAppStore((s) => s.updateNode); const canvasMode = useAppStore((s) => s.canvasMode);
return ( <> <NodeResizer isVisible={selected && canvasMode === 'editing'} minWidth={200} minHeight={100} /> <div className="node-container"> <Handle type="target" position={Position.Top} />
<div className="node-header">
<span className="node-title">{data.title}</span>
</div>
<div className="node-body">
{/* Node-specific content */}
</div>
<Handle type="source" position={Position.Bottom} />
</div>
</>
); });
Type Definition Pattern
import type { Node } from '@xyflow/react';
// Data interface — must extend Record<string, unknown> export interface {{NodeName}}Data extends Record<string, unknown> { title: string; description?: string; // Add node-specific fields here }
// Typed node alias export type {{NodeName}} = Node<{{NodeName}}Data, '{{nodeType}}'>;
Union Type for All Nodes
export type AppNode = | Node<TextNodeData, 'text-node'> | Node<VideoNodeData, 'video-node'> | Node<{{NodeName}}Data, '{{nodeType}}'>;
Handle Configuration
// Single input, single output (most common) <Handle type="target" position={Position.Top} /> <Handle type="source" position={Position.Bottom} />
// Multiple named handles <Handle type="target" position={Position.Left} id="input-a" /> <Handle type="target" position={Position.Left} id="input-b" style={{ top: '75%' }} /> <Handle type="source" position={Position.Right} id="output" />
// Conditional handle (only show in editing mode) {canvasMode === 'editing' && ( <Handle type="source" position={Position.Bottom} /> )}
Store Integration (Zustand)
// In app-store.ts interface AppState { nodes: AppNode[]; updateNode: (id: string, data: Partial<AppNode['data']>) => void; // ... other actions }
export const useAppStore = create<AppState>((set, get) => ({ nodes: [], updateNode: (id, data) => set({ nodes: get().nodes.map((n) => n.id === id ? { ...n, data: { ...n.data, ...data } } : n ), }), }));
Default Node Data
// defaults.ts export const DEFAULT_NODE_DATA: Record<string, () => AppNode['data']> = { '{{nodeType}}': () => ({ title: 'New {{NodeName}}', description: '', }), };
Registration
// nodeTypes.ts — register all custom nodes import { {{NodeName}} } from '@/components/nodes/{{NodeName}}';
export const nodeTypes = { '{{nodeType}}': {{NodeName}}, // ... other node types } as const;
// Canvas.tsx import { ReactFlow } from '@xyflow/react'; import { nodeTypes } from './nodeTypes';
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} />
Integration Steps
-
Add type — Define {{NodeName}}Data interface in types/index.ts
-
Create component — Build node in components/nodes/{{NodeName}}.tsx
-
Export — Add to components/nodes/index.ts barrel export
-
Add defaults — Register default data in store/app-store.ts
-
Register — Add to nodeTypes object for React Flow
-
Add to menus — Include in AddBlockMenu and ConnectMenu
Common Patterns
Editable Title
<input className="node-title-input" value={data.title} onChange={(e) => updateNode(id, { title: e.target.value })} onBlur={() => /* save */} />
Status Indicator
<div className={status-dot status-${data.status}} />
Validation Badge
{data.errors?.length > 0 && ( <span className="error-badge">{data.errors.length}</span> )}
Anti-Patterns
Avoid Why Instead
Heavy computation in node render Blocks canvas interaction useMemo or move to store
Inline styles for layout Inconsistent, hard to maintain CSS classes or Tailwind
Forgetting memo() wrapper Unnecessary re-renders on pan/zoom Always wrap with memo
Untyped node data Runtime errors, poor DX Always define data interface
Direct DOM manipulation Breaks React Flow internals Use React state + handles