NYC
skills/thebushidocollective/han/reactflow-fundamentals

reactflow-fundamentals

SKILL.md

React Flow Fundamentals

Build customizable node-based editors and interactive diagrams with React Flow. This skill covers core concepts, setup, and common patterns for creating flow-based interfaces.

Installation

# npm
npm install @xyflow/react

# pnpm
pnpm add @xyflow/react

# yarn
yarn add @xyflow/react

Basic Setup

import { useCallback } from 'react';
import {
  ReactFlow,
  useNodesState,
  useEdgesState,
  addEdge,
  Background,
  Controls,
  MiniMap,
  type Node,
  type Edge,
  type OnConnect,
} from '@xyflow/react';

import '@xyflow/react/dist/style.css';

const initialNodes: Node[] = [
  {
    id: '1',
    type: 'input',
    data: { label: 'Start' },
    position: { x: 250, y: 0 },
  },
  {
    id: '2',
    data: { label: 'Process' },
    position: { x: 250, y: 100 },
  },
  {
    id: '3',
    type: 'output',
    data: { label: 'End' },
    position: { x: 250, y: 200 },
  },
];

const initialEdges: Edge[] = [
  { id: 'e1-2', source: '1', target: '2' },
  { id: 'e2-3', source: '2', target: '3' },
];

export default function Flow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onConnect: OnConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges]
  );

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        fitView
      >
        <Background />
        <Controls />
        <MiniMap />
      </ReactFlow>
    </div>
  );
}

Node Types

Built-in Node Types

const nodes: Node[] = [
  // Input node - only has source handles
  {
    id: '1',
    type: 'input',
    data: { label: 'Input Node' },
    position: { x: 0, y: 0 },
  },
  // Default node - has both source and target handles
  {
    id: '2',
    type: 'default',
    data: { label: 'Default Node' },
    position: { x: 0, y: 100 },
  },
  // Output node - only has target handles
  {
    id: '3',
    type: 'output',
    data: { label: 'Output Node' },
    position: { x: 0, y: 200 },
  },
];

Node Configuration

const node: Node = {
  id: 'unique-id',
  type: 'default',
  position: { x: 100, y: 100 },
  data: { label: 'My Node', customProp: 'value' },
  // Optional properties
  style: { backgroundColor: '#f0f0f0' },
  className: 'custom-node',
  sourcePosition: Position.Right,
  targetPosition: Position.Left,
  draggable: true,
  selectable: true,
  connectable: true,
  deletable: true,
  hidden: false,
  selected: false,
  dragging: false,
  zIndex: 0,
  extent: 'parent', // Constrain to parent node
  parentId: 'parent-node-id', // For nested nodes
  expandParent: true, // Expand parent when node is outside bounds
};

Edge Types

Built-in Edge Types

import { MarkerType } from '@xyflow/react';

const edges: Edge[] = [
  // Default edge (bezier curve)
  {
    id: 'e1',
    source: '1',
    target: '2',
    type: 'default',
  },
  // Straight line
  {
    id: 'e2',
    source: '2',
    target: '3',
    type: 'straight',
  },
  // Step edge (right angles)
  {
    id: 'e3',
    source: '3',
    target: '4',
    type: 'step',
  },
  // Smoothstep edge (rounded corners)
  {
    id: 'e4',
    source: '4',
    target: '5',
    type: 'smoothstep',
  },
];

Edge Configuration

const edge: Edge = {
  id: 'edge-id',
  source: 'source-node-id',
  target: 'target-node-id',
  // Optional properties
  type: 'smoothstep',
  sourceHandle: 'handle-a',
  targetHandle: 'handle-b',
  label: 'Edge Label',
  labelStyle: { fill: '#333', fontWeight: 700 },
  labelBgStyle: { fill: '#fff' },
  labelBgPadding: [8, 4],
  labelBgBorderRadius: 4,
  style: { stroke: '#333', strokeWidth: 2 },
  animated: true,
  markerEnd: {
    type: MarkerType.ArrowClosed,
    color: '#333',
  },
  markerStart: {
    type: MarkerType.Arrow,
  },
  interactionWidth: 20,
  deletable: true,
  selectable: true,
  selected: false,
  hidden: false,
  zIndex: 0,
  data: { customProp: 'value' },
};

Handles

import { Handle, Position, type NodeProps } from '@xyflow/react';

function CustomNode({ data }: NodeProps) {
  return (
    <div className="custom-node">
      {/* Target handle (input) */}
      <Handle
        type="target"
        position={Position.Top}
        id="input"
        style={{ background: '#555' }}
        isConnectable={true}
      />

      <div>{data.label}</div>

      {/* Multiple source handles */}
      <Handle
        type="source"
        position={Position.Bottom}
        id="output-a"
        style={{ left: '25%', background: '#555' }}
      />
      <Handle
        type="source"
        position={Position.Bottom}
        id="output-b"
        style={{ left: '75%', background: '#555' }}
      />
    </div>
  );
}

Plugin Components

Background

import { Background, BackgroundVariant } from '@xyflow/react';

<ReactFlow nodes={nodes} edges={edges}>
  {/* Dots pattern */}
  <Background variant={BackgroundVariant.Dots} gap={12} size={1} />

  {/* Lines pattern */}
  <Background variant={BackgroundVariant.Lines} gap={20} />

  {/* Cross pattern */}
  <Background variant={BackgroundVariant.Cross} gap={25} />

  {/* Custom styling */}
  <Background
    color="#aaa"
    gap={16}
    size={1}
    variant={BackgroundVariant.Dots}
  />
</ReactFlow>

Controls

import { Controls } from '@xyflow/react';

<ReactFlow nodes={nodes} edges={edges}>
  <Controls
    showZoom={true}
    showFitView={true}
    showInteractive={true}
    position="bottom-left"
  />
</ReactFlow>

MiniMap

import { MiniMap } from '@xyflow/react';

<ReactFlow nodes={nodes} edges={edges}>
  <MiniMap
    nodeColor={(node) => {
      switch (node.type) {
        case 'input':
          return '#0041d0';
        case 'output':
          return '#ff0072';
        default:
          return '#1a192b';
      }
    }}
    nodeStrokeWidth={3}
    zoomable
    pannable
  />
</ReactFlow>

Panel

import { Panel } from '@xyflow/react';

<ReactFlow nodes={nodes} edges={edges}>
  <Panel position="top-left">
    <button onClick={onSave}>Save</button>
    <button onClick={onRestore}>Restore</button>
  </Panel>

  <Panel position="top-right">
    <div>Node count: {nodes.length}</div>
  </Panel>
</ReactFlow>

Event Handling

import {
  ReactFlow,
  type NodeMouseHandler,
  type EdgeMouseHandler,
  type OnSelectionChangeFunc,
} from '@xyflow/react';

function Flow() {
  // Node events
  const onNodeClick: NodeMouseHandler = useCallback((event, node) => {
    console.log('Node clicked:', node.id);
  }, []);

  const onNodeDoubleClick: NodeMouseHandler = useCallback((event, node) => {
    console.log('Node double clicked:', node.id);
  }, []);

  const onNodeDragStart: NodeMouseHandler = useCallback((event, node) => {
    console.log('Drag started:', node.id);
  }, []);

  const onNodeDrag: NodeMouseHandler = useCallback((event, node) => {
    console.log('Dragging:', node.position);
  }, []);

  const onNodeDragStop: NodeMouseHandler = useCallback((event, node) => {
    console.log('Drag stopped:', node.position);
  }, []);

  // Edge events
  const onEdgeClick: EdgeMouseHandler = useCallback((event, edge) => {
    console.log('Edge clicked:', edge.id);
  }, []);

  // Selection changes
  const onSelectionChange: OnSelectionChangeFunc = useCallback(
    ({ nodes, edges }) => {
      console.log('Selected nodes:', nodes);
      console.log('Selected edges:', edges);
    },
    []
  );

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodeClick={onNodeClick}
      onNodeDoubleClick={onNodeDoubleClick}
      onNodeDragStart={onNodeDragStart}
      onNodeDrag={onNodeDrag}
      onNodeDragStop={onNodeDragStop}
      onEdgeClick={onEdgeClick}
      onSelectionChange={onSelectionChange}
    />
  );
}

Viewport Control

import { useReactFlow } from '@xyflow/react';

function ViewportControls() {
  const { zoomIn, zoomOut, fitView, setCenter, setViewport, getViewport } =
    useReactFlow();

  return (
    <div>
      <button onClick={() => zoomIn()}>Zoom In</button>
      <button onClick={() => zoomOut()}>Zoom Out</button>
      <button onClick={() => fitView({ padding: 0.2 })}>Fit View</button>
      <button onClick={() => setCenter(0, 0, { zoom: 1 })}>Center</button>
      <button
        onClick={() => {
          const viewport = getViewport();
          console.log('Current viewport:', viewport);
        }}
      >
        Log Viewport
      </button>
    </div>
  );
}

// Must be used inside ReactFlowProvider
function App() {
  return (
    <ReactFlowProvider>
      <Flow />
      <ViewportControls />
    </ReactFlowProvider>
  );
}

Node Operations with useReactFlow

import { useReactFlow, type Node } from '@xyflow/react';

function NodeOperations() {
  const { getNodes, setNodes, getNode, addNodes, deleteElements } =
    useReactFlow();

  const addNewNode = () => {
    const newNode: Node = {
      id: `node-${Date.now()}`,
      data: { label: 'New Node' },
      position: { x: Math.random() * 300, y: Math.random() * 300 },
    };
    addNodes(newNode);
  };

  const updateNode = (id: string, data: object) => {
    setNodes((nodes) =>
      nodes.map((node) =>
        node.id === id ? { ...node, data: { ...node.data, ...data } } : node
      )
    );
  };

  const deleteNode = (id: string) => {
    deleteElements({ nodes: [{ id }] });
  };

  const getAllNodes = () => {
    const nodes = getNodes();
    console.log('All nodes:', nodes);
  };

  return (
    <div>
      <button onClick={addNewNode}>Add Node</button>
      <button onClick={getAllNodes}>Log Nodes</button>
    </div>
  );
}

Saving and Restoring State

import { useReactFlow, type ReactFlowJsonObject } from '@xyflow/react';

function SaveRestore() {
  const { toObject, setNodes, setEdges, setViewport } = useReactFlow();

  const onSave = useCallback(() => {
    const flow = toObject();
    localStorage.setItem('flow', JSON.stringify(flow));
  }, [toObject]);

  const onRestore = useCallback(() => {
    const restoreFlow = async () => {
      const flow = JSON.parse(
        localStorage.getItem('flow') || '{}'
      ) as ReactFlowJsonObject;

      if (flow.nodes && flow.edges) {
        setNodes(flow.nodes);
        setEdges(flow.edges);
        if (flow.viewport) {
          setViewport(flow.viewport);
        }
      }
    };

    restoreFlow();
  }, [setNodes, setEdges, setViewport]);

  return (
    <Panel position="top-right">
      <button onClick={onSave}>Save</button>
      <button onClick={onRestore}>Restore</button>
    </Panel>
  );
}

When to Use This Skill

Use reactflow-fundamentals when you need to:

  • Build workflow builders or no-code editors
  • Create data pipeline visualizations
  • Design state machine diagrams
  • Build chatbot conversation flows
  • Create organizational charts
  • Design electrical circuit diagrams
  • Build ML pipeline visualizers
  • Create interactive decision trees

Best Practices

  • Use unique IDs for nodes and edges
  • Memoize callbacks with useCallback
  • Use TypeScript for type safety
  • Keep node components pure and performant
  • Use CSS classes instead of inline styles for complex styling
  • Store flow state in a central state manager for complex apps
  • Use fitView() on initial render for better UX
  • Add keyboard shortcuts for common operations
  • Implement undo/redo for better user experience
  • Use node validation before connections

Resources

Weekly Installs
14
First Seen
8 days ago
Installed on
github-copilot14
codex14
gemini-cli14
opencode13
amp13
kimi-cli13