React Flow best practices for workflow canvas in seer-frontend. Use when modifying WorkflowCanvas, adding new block types, debugging node rendering issues, or optimizing React Flow performance.
This Skill helps you work with React Flow (@xyflow/react) for building the workflow canvas in seer-frontend. Use this when creating new block nodes, modifying canvas behavior, or debugging rendering issues.
src/components/workflows/canvas/WorkflowCanvas.tsx)The main canvas component that orchestrates React Flow.
Key patterns:
canvasStore for state managementapplyNodeChanges and applyEdgeChangesNode type registration:
const nodeTypes = {
tool: ToolBlockNode,
llm: LLMBlockNode,
if_else: IfElseBlockNode,
for_loop: ForLoopBlockNode,
trigger: TriggerBlockNode,
};
State management pattern:
const {
nodes,
edges,
setNodes,
setEdges,
selectedNodeId,
updateNode,
} = useCanvasStore(
useShallow((state) => ({
nodes: state.nodes,
edges: state.edges,
setNodes: state.setNodes,
setEdges: state.setEdges,
selectedNodeId: state.selectedNodeId,
updateNode: state.updateNode,
})),
);
Props interface:
triggerNodes: Optional trigger nodespreviewGraph: Optional preview graph for read-only displayonNodeDoubleClick: Handler for node double-clicks (opens config dialog)onNodeDrop: Handler for drag-and-drop block placementclassName: Optional CSS classesreadOnly: Disables editing when truesrc/components/workflows/blocks/BaseBlockNode.tsx)The foundation for all custom block nodes.
Key patterns:
memo() for performance optimizationHandle components for connectionsProps interface:
interface BaseBlockNodeProps extends NodeProps<WorkflowNode> {
icon?: React.ReactNode; // Icon for the block
color?: string; // Color theme (primary, success, warning, etc.)
handles?: {
inputs?: string[]; // Input handle IDs
outputs?: string[]; // Output handle IDs
};
children?: React.ReactNode; // Block content (badges, config, etc.)
minWidth?: string; // Minimum width (default: 320px)
}
Visual feedback pattern:
className={cn(
'relative px-4 py-3 rounded-lg border-2 transition-[border,shadow,ring]',
selected
? 'border-primary shadow-lg ring-2 ring-primary ring-offset-2'
: 'border-border bg-card hover:border-primary/50',
)}
Handle positioning:
left: -8px, centered verticallyright: -8px, centered vertically!w-3 !h-3 !bg-border !border-2 !border-backgroundsrc/stores/canvasStore.ts)Manages canvas state including nodes, edges, and selection.
Key state:
interface CanvasStore {
nodes: Node<WorkflowNodeData>[];
edges: WorkflowEdge[];
selectedNodeId: string | null;
setNodes: (nodes: Node<WorkflowNodeData>[]) => void;
setEdges: (edges: WorkflowEdge[]) => void;
setSelectedNodeId: (nodeId: string | null) => void;
updateNode: (nodeId: string, updates: Partial<WorkflowNodeData>) => void;
}
State synchronization:
useShallow from zustand/shallow prevents unnecessary re-renderssrc/stores/workflowStore.ts)Manages workflow metadata and API interactions.
Key patterns:
graphToWorkflowSpec and workflowSpecToGraph for serializationGraph conversion:
// Convert React Flow graph to backend spec
const spec = graphToWorkflowSpec(graph);
// Convert backend spec to React Flow graph
const graph = workflowSpecToGraph(spec);
Follow these steps to add a new block type to the workflow canvas:
Create a new file in src/components/workflows/blocks/ (e.g., MyBlockNode.tsx):
import { memo } from 'react';
import { NodeProps, type Node } from '@xyflow/react';
import { MyIcon } from 'lucide-react';
import { BaseBlockNode } from './BaseBlockNode';
import { WorkflowNodeData } from '../types';
import { Badge } from '@/components/ui/badge';
type MyBlockNode = Node<WorkflowNodeData>;
export const MyBlockNode = memo(function MyBlockNode(props: NodeProps<MyBlockNode>) {
const { data } = props;
return (
<BaseBlockNode
{...props}
icon={<MyIcon className="w-4 h-4 text-primary" />}
color="primary"
handles={{
inputs: ['input'],
outputs: ['output'],
}}
>
{/* Optional: Add badges or status indicators */}
<Badge variant="secondary" className="text-[10px]">
{data.config?.someProperty}
</Badge>
</BaseBlockNode>
);
});
Add the new node type to canvas/WorkflowCanvas.tsx:
import { MyBlockNode } from './blocks/MyBlockNode';
const nodeTypes = {
tool: ToolBlockNode,
llm: LLMBlockNode,
if_else: IfElseBlockNode,
for_loop: ForLoopBlockNode,
trigger: TriggerBlockNode,
my_block: MyBlockNode, // Add your new block here
};
Update src/components/workflows/types.ts with the new block config:
export interface MyBlockConfig {
someProperty: string;
anotherProperty?: number;
}
// Add to WorkflowNodeData type union
export type WorkflowNodeData = {
type: 'my_block';
label: string;
config: MyBlockConfig;
// ... other common fields
} | /* other block types */;
Update the block palette to include the new block for drag-and-drop:
const blockPalette = [
// ... existing blocks
{
type: 'my_block',
label: 'My Block',
icon: <MyIcon className="w-4 h-4" />,
defaultConfig: {
someProperty: 'default',
},
},
];
Use memo() for custom nodes:
export const MyBlockNode = memo(function MyBlockNode(props) {
// Component implementation
});
Use useShallow for store subscriptions:
const { nodes, edges } = useCanvasStore(
useShallow((state) => ({
nodes: state.nodes,
edges: state.edges,
})),
);
Prevent event bubbling in interactive children:
<div onPointerDown={(event) => event.stopPropagation()}>
<Button>Click Me</Button>
</div>
Validate connections before allowing edges to be created:
const isValidConnection = useCallback((connection: Connection) => {
// Prevent self-connections
if (connection.source === connection.target) {
return false;
}
// Custom validation logic
const sourceNode = nodes.find((n) => n.id === connection.source);
const targetNode = nodes.find((n) => n.id === connection.target);
// Add your validation rules here
return true;
}, [nodes]);
<ReactFlow
isValidConnection={isValidConnection}
// ... other props
/>
Highlight selected nodes in the canvas:
const renderedNodes = useMemo(() => {
return nodes.map((node) => ({
...node,
data: {
...node.data,
selected: node.id === selectedNodeId,
},
}));
}, [nodes, selectedNodeId]);
Customize edge appearance with markers and styles:
const edges = useMemo(() => {
return workflowEdges.map((edge) => ({
...edge,
type: 'smoothstep',
animated: false,
markerEnd: {
type: MarkerType.ArrowClosed,
width: 20,
height: 20,
},
}));
}, [workflowEdges]);
Implement drop handler for adding blocks:
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const blockData = JSON.parse(event.dataTransfer.getData('application/json'));
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
onNodeDrop?.(blockData, position);
},
[onNodeDrop, screenToFlowPosition],
);
<div onDrop={onDrop} onDragOver={(e) => e.preventDefault()}>
<ReactFlow />
</div>
Use semantic colors for different block types:
seer (purple) - AI-powered operationssuccess (emerald) - External integrationsprimary (default) - Control flowwarning (amber) - Event triggers1. Nodes not rendering:
nodeTypes objectWorkflowNodeData typeid, type, position, and data fields2. Selection not updating:
selectedNodeId is synchronized with storeselected prop is passed to custom node components3. Edges not connecting:
type prop (target/source)isValidConnection for custom validation4. Performance issues with large graphs:
memo() for all custom node componentsuseShallow for store subscriptions5. State not persisting:
updateNode updates both store and React Flow stateapplyNodeChanges and applyEdgeChanges are used for React Flow changesTest workflow canvas with backend:
# Start frontend dev server
npm run dev
# Start backend API (in seer directory)
cd /Users/pika/Projects/seer
python -m api.main
| File | Purpose | When to Modify |
|---|---|---|
src/components/workflows/canvas/WorkflowCanvas.tsx | Main canvas component | Adding global canvas features |
src/components/workflows/blocks/BaseBlockNode.tsx | Base block component | Changing common block behavior |
src/components/workflows/blocks/*.tsx | Custom block nodes | Adding/modifying specific block types |
src/stores/canvasStore.ts | Canvas state management | Adding canvas state or actions |
src/stores/workflowStore.ts | Workflow CRUD operations | Adding workflow API calls |
src/components/workflows/types.ts | Type definitions | Adding new block types or configs |
.claude/rules/component-design.md | UI component patterns | Following badge/button patterns |
.claude/rules/color-theming.md | Color system | Applying semantic colors |
When working with React Flow:
memo()nodeTypes object