React Flow Advanced

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

作者 existential-birdsd1a74899fbfec74974d1818e4cac7c3d54d44b65無授權條款83 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫3 週前更新

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.

AI 產生的概覽

進階 React Flow 模式:子流程、自訂連線、拖放、復原重做、dagre 版面配置與狀態選擇器。

功能
提供 React Flow 複雜圖形介面的實作指引與程式碼片段:巢狀子流程與群組節點、自訂連接線、外部拖放、復原重做歷程、以 dagre 為基礎的程式化版面配置、拖曳連線建立節點、從邊取得節點資料、節點變更中介層、鍵盤快速鍵,以及記憶化的 store 選擇器。同時為每種模式列出依序執行的出貨前檢查項目,例如驗證 parentId 參照、拖放座標轉換與復原重做的一致性。
適用情境
適用於建置或除錯需要巢狀群組、自訂連線繪製、拖放建立節點、復原重做、自動版面配置或訂閱效能調校的 React Flow 畫布。適合複雜圖形編輯器,而非簡單靜態圖表。適合已在 React 專案中使用 React Flow 的開發者。
執行需求
不附帶指令碼,僅提供說明與程式碼片段。需要已安裝 @xyflow/react 的 React 專案,程式化版面配置另需 dagre,淺比較選擇器需要 zustand。

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

來源與署名

來源:existential-birds/beagle位於plugins/beagle-react/skills/react-flow-advanced提交d1a7489

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架