react-flow-node-ts
Originally frommicrosoft/skills
SKILL.md
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}}Datainterface intypes/index.ts - Create component — Build node in
components/nodes/{{NodeName}}.tsx - Export — Add to
components/nodes/index.tsbarrel export - Add defaults — Register default data in
store/app-store.ts - Register — Add to
nodeTypesobject 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 |
Weekly Installs
3
Repository
sivag-lab/roth_mcpGitHub Stars
1
First Seen
5 days ago
Security Audits
Installed on
opencode3
claude-code3
github-copilot3
codex3
kimi-cli3
gemini-cli3