React Flow Advanced

existential-birds/beagle/plugins/beagle-react/skills/react-flow-advanced

by existential-birdsd1a74899fbfec74974d1818e4cac7c3d54d44b65No license83 starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 3 weeks ago

Advanced React Flow patterns for complex use cases. Use when implementing sub-flows, custom connection lines, programmatic layouts, drag-and-drop, undo/redo, or complex state synchronization.

Instructions onlySoftware Development
AI-generated overview

Advanced React Flow patterns for sub-flows, custom connection lines, drag-and-drop, undo/redo, dagre layout and store selectors.

What it does
Provides implementation guidance and code snippets for advanced React Flow graph interfaces: nested sub-flows and group nodes, custom connection lines, external drag-and-drop, undo/redo history, dagre-based programmatic layout, connect-on-drop, edge data access, node-change middleware, keyboard shortcuts and memoized store selectors. It also lists sequenced pre-ship checks for each pattern, such as verifying parentId references, drop position conversion and undo/redo consistency.
When to use it
Use when building or debugging a React Flow canvas that needs nested groups, custom connection rendering, drag-and-drop node creation, undo/redo, automatic layout or performance tuning of store subscriptions. It suits complex graph editors rather than simple static diagrams. Suitable for developers already working in a React codebase with React Flow installed. Suitable for developers already working in a React codebase with React Flow installed.
Requirements
No scripts ship; instructions and code snippets only. Requires a React project using the @xyflow/react package, plus dagre for programmatic layout and zustand for shallow selector comparison.

Advanced React Flow Patterns

Gates (check before shipping)

Use these as sequenced checks—not “I think it works.”

  1. Sub-flows / groups: Pass: Every parentId matches an existing node id; the parent type is registered in nodeTypes; child positions are relative to the parent as intended (spot-check one drag inside/outside the group).
  2. Custom connection line: Pass: With a valid/invalid drag, stroke or connectionStatus visibly differs; path renders without console errors from getSmoothStepPath (invalid coords).
  3. External drag-and-drop: Pass: onDragOver always preventDefault(); drop position uses screenToFlowPosition (not raw clientX/clientY as flow coords); new node appears under the cursor on the pane.
  4. Undo/redo: Pass: One undo returns to the prior { nodes, edges }; redo restores; rapid changes do not leave canUndo/canRedo inconsistent with visible graph (exercise add → undo → redo once).
  5. Programmatic layout (dagre): Pass: After setNodes, node positions match intended rankdir; fitView runs after layout (e.g. requestAnimationFrame) so the viewport is not stale.
  6. Connect on drop (new node): Pass: Dropping on empty pane creates a node and an edge from the source handle; dropping on a valid target does not duplicate nodes (only the invalid-drop path adds a node).
  7. Selectors / store: Pass: Components that useStore with objects use shallow (or equivalent) so unrelated store updates do not re-render every frame.

Sub-Flows (Nested Nodes)

tsx
const nodes = [  // Parent (group) node  {    id: 'group-1',    type: 'group',    position: { x: 0, y: 0 },    style: { width: 400, height: 300, padding: 10 },    data: { label: 'Group' },  },  // Child nodes  {    id: 'child-1',    parentId: 'group-1',        // Reference parent    extent: 'parent',           // Constrain to parent bounds    expandParent: true,         // Auto-expand parent if dragged to edge    position: { x: 20, y: 50 }, // Relative to parent    data: { label: 'Child 1' },  },  {    id: 'child-2',    parentId: 'group-1',    extent: 'parent',    position: { x: 200, y: 50 },    data: { label: 'Child 2' },  },];

Group Node Component

tsx
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>  );}

Custom Connection Line

tsx
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} />

Drag and Drop from External Source

tsx
import { useCallback, useRef, useState } from 'react';import { useReactFlow } from '@xyflow/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;
    // Convert screen position to flow position    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 componentfunction 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>  );}

Undo/Redo

tsx
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;
      // Remove future states and add new state      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 };}
// Usagefunction Flow() {  const {    state: { nodes, edges },    setState,    undo, redo, canUndo, canRedo  } = useUndoRedo({ nodes: initialNodes, edges: initialEdges });
  // Capture state on significant changes  const onNodesChange = useCallback((changes) => {    const hasPositionChange = changes.some(c => c.type === 'position' && !c.dragging);    if (hasPositionChange) {      setState(prev => ({        nodes: applyNodeChanges(changes, prev.nodes),        edges: prev.edges,      }));    }  }, [setState]);}

Programmatic Layout with dagre

tsx
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 measuredfunction 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]);}

Connection with Edge on Drop

tsx
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) => {      // Only proceed if dropped on pane (not on a node)      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}    />  );}

Accessing Node Data from Edges

tsx
import { useNodesData, type EdgeProps } from '@xyflow/react';
function DataEdge({ source, target, ...props }: EdgeProps) {  // Get data for source and target nodes  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>    </>  );}

Middleware for Node Changes

tsx
// Filter or modify changes before they're appliedconst onNodesChangeMiddleware = useCallback((changes: NodeChange[]) => {  // Example: Prevent deletion of certain nodes  const filteredChanges = changes.filter((change) => {    if (change.type === 'remove') {      const node = nodes.find((n) => n.id === change.id);      return node?.data?.deletable !== false;    }    return true;  });
  setNodes((nds) => applyNodeChanges(filteredChanges, nds));}, [nodes, setNodes]);

Keyboard Shortcuts

tsx
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]);
  // Custom 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]);}

Performance: Memoizing Selectors

tsx
import { useCallback } from 'react';import { useStore, type ReactFlowState } from '@xyflow/react';import { shallow } from 'zustand/shallow';
// Create stable selector outside componentconst nodesSelector = (state: ReactFlowState) => state.nodes;
// Or use multiple values with shallow compareconst 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>;}

Source and attribution

Source:existential-birds/beagleinplugins/beagle-react/skills/react-flow-advancedat commitd1a7489

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal