Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
existential-birds avatar

React Flow Advanced

  • 575 installs
  • 74 repo stars
  • Updated July 21, 2026
  • existential-birds/beagle

react-flow-advanced is a Claude Code skill that implements complex node-based editors and diagrams in React with sub-flows, custom edges, layout engines, and undo/redo while avoiding common React Flow footguns for develo

About

react-flow-advanced is an advanced patterns skill from existential-birds/beagle for React Flow complex use cases. It provides sequenced shipping gates for sub-flows and groups (parentId validation, registered nodeTypes, relative child positions), custom connection lines with visible valid/invalid stroke states, external drag-and-drop with preventDefault on dragOver and correct screen-to-flow coordinates, programmatic layouts, undo/redo stacks, and complex state synchronization. Developers reach for it when a node editor outgrows basic examples and needs grouped nodes, custom edges, or reliable interaction polish. Each gate is a pass/fail checklist—not subjective review—so teams catch React Flow integration bugs before release. The skill targets production diagram builders, workflow canvases, and visual programming UIs.

  • Six sequenced shipping gates for sub-flows, connection lines, DnD, undo/redo, dagre layout, and connect-on-drop
  • Sub-flow checks: parentId, registered parent type, and relative child positioning after drag inside/outside groups
  • External drop pipeline: preventDefault on dragOver plus screenToFlowPosition (not raw client coords)
  • Undo/redo consistency: add → undo → redo must match visible graph and canUndo/canRedo state
  • Programmatic layout: setNodes after dagre rankdir plus fitView on requestAnimationFrame to avoid stale viewport

React Flow Advanced by the numbers

  • 575 all-time installs (skills.sh)
  • Ranked #587 of 2,245 Frontend Development skills by installs in the Skillselion catalog
  • Security screen: MEDIUM risk (skills.sh audit)
  • Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill react-flow-advanced

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs575
repo stars74
Security audit3 / 3 scanners passed
Last updatedJuly 21, 2026
Repositoryexistential-birds/beagle

How do you build complex node editors with React Flow?

Ship complex node-based editors and diagrams in React with sub-flows, custom edges, layout engines, and undo/redo without missing React Flow footguns.

Who is it for?

React developers shipping node-based editors, workflow canvases, or diagram builders that exceed basic React Flow tutorials.

Skip if: Projects needing simple static flowcharts without sub-flows, custom edges, or advanced interaction and layout requirements.

When should I use this skill?

The developer implements React Flow sub-flows, custom connection lines, programmatic layouts, drag-and-drop, or undo/redo in a complex editor.

What you get

Grouped sub-flow nodes, custom edges, layout-synced canvas, drag-and-drop handlers, and undo/redo state passing sequenced React Flow gates.

  • Sub-flow group nodes
  • Custom edge components
  • Undo/redo interaction layer

Files

SKILL.mdMarkdownGitHub ↗

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)

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

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

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

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 component
function 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

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

// Usage
function 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

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 measured
function 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

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

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

// Filter or modify changes before they're applied
const 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

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

import { useCallback } from 'react';
import { useStore, type ReactFlowState } from '@xyflow/react';
import { shallow } from 'zustand/shallow';

// Create stable selector outside component
const nodesSelector = (state: ReactFlowState) => state.nodes;

// Or use multiple values with shallow compare
const 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>;
}

Related skills

How it compares

Pick react-flow-advanced over basic React Flow docs when shipping grouped nodes, custom edges, or undo/redo with explicit pass/fail gates.

FAQ

What sub-flow checks does react-flow-advanced require?

react-flow-advanced requires every parentId to match an existing node id, the parent type to be registered in nodeTypes, and child positions to stay relative to the parent. Developers spot-check dragging inside and outside the group before shipping.

How does react-flow-advanced validate custom connection lines?

react-flow-advanced checks that valid and invalid drags produce visibly different strokes or connectionStatus values, and that getSmoothStepPath renders without console errors from invalid coordinates during connection dragging.

Is React Flow Advanced safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.