# How Modly Uses React Flow to Power Its Visual Workflow System

> Discover how Modly uses React Flow to build a powerful visual workflow system. Learn about custom nodes, connection validation, and history for a desktop grade editor.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: how-to-guide
- Published: 2026-08-21

---

**Modly leverages the `@xyflow/react` library to render an interactive node-based canvas, wrapping React Flow’s core graph engine with custom node types, connection validation, and a history system to create a desktop-grade visual workflow editor.**

Modly implements its visual workflow system using React Flow to manage complex 3D asset pipelines. The open-source application maintained by `lightningpixel/modly` extends the library’s primitives with domain-specific logic for drag-and-drop interactions, type-safe connections, and persistent state management.

## Core Architecture

The workflow editor centers on [`WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/WorkflowsPage.tsx), which orchestrates the React Flow provider context and global state.

### Canvas Setup and State Management

The entry point wraps the canvas in a `ReactFlowProvider` to expose the flow context to all child components. According to the source code in [`src/areas/workflows/WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/WorkflowsPage.tsx), the application initializes node and edge state using React Flow’s native hooks:

```tsx
const [nodes, setNodes] = useNodesState(workflow.nodes);
const [edges, setEdges] = useEdgesState(workflow.edges);

```

These state arrays drive the visual graph and are persisted to the workspace on every change. The `ReactFlow` component receives these states along with change handlers:

```tsx
<ReactFlow
  nodes={nodes}
  edges={edges}
  onNodesChange={setNodes}
  onEdgesChange={setEdges}
  onConnect={(params) => setEdges((eds) => addEdge(params, eds))}
  isValidConnection={isValidConnection}
/>

```

### Custom Node Types

Modly defines specialized node components that import `useReactFlow` to manipulate their own data. In [`src/areas/workflows/nodes/BaseNode.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/BaseNode.tsx), the base implementation exposes common UI elements including handles and resize controls. Specific node types extend this pattern:

- **ExtensionNode.tsx**: Updates extension parameters via `updateNodeData`
- **WhileNode.tsx**: Implements container logic using `NodeResizer` to group child nodes with `parentId` relationships

Each node calls `useReactFlow` methods like `deleteElements` to remove themselves from the graph or `updateNodeData` to sync configuration changes.

### Connection Validation and Custom Edges

The system prevents invalid topologies through the `isValidConnection` callback defined in [`WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/WorkflowsPage.tsx). This function checks type compatibility between source and target handles and performs cycle detection by walking upstream edges:

```typescript
const isValidConnection = useCallback((connection) => {
  const srcType = getNodeOutputType(getNode(connection.source), allExtensions);
  const tgtType = getNodeInputType(getNode(connection.target), connection.targetHandle, allExtensions);
  if (srcType && tgtType && srcType !== tgtType) return false;

  // Cycle detection logic
  const stack = [connection.target];
  const seen = new Set<string>();
  while (stack.length) {
    const id = stack.pop()!;
    if (id === connection.source) return false;
    if (seen.has(id)) continue;
    seen.add(id);
    edges.filter(e => e.source === id).forEach(e => stack.push(e.target));
  }
  return true;
}, [getNode, allExtensions, edges]);

```

For rendering, [`src/areas/workflows/nodes/WorkflowEdge.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/WorkflowEdge.tsx) imports `getBezierPath` from React Flow to draw curved connections between nodes.

## Interaction Patterns

### Drag-and-Drop Node Creation

When users drop items onto the canvas, Modly translates screen coordinates to flow coordinates using `screenToFlowPosition`. The `onDrop` handler in [`WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/WorkflowsPage.tsx) demonstrates this mapping:

```tsx
const onDrop = useCallback((e: React.DragEvent) => {
  const pos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
  const nodeType = e.dataTransfer.getData('modly/node-type');
  
  if (nodeType) {
    setNodes((nds) => [
      ...nds,
      {
        id: crypto.randomUUID(),
        type: nodeType,
        position: pos,
        data: { enabled: true, params: {} },
      },
    ]);
  }
}, [screenToFlowPosition, setNodes]);

```

This handler supports both individual nodes and container nodes that establish parent-child relationships in the data structure.

## State Persistence and History

### Auto-Save Integration

Modly maintains a snapshot array of `{nodes, edges}` and pushes new snapshots after a debounce period. The application calls `setNodes` and `setEdges` directly to update React Flow’s internal state, triggering re-renders and persistence logic.

### Undo/Redo System

A history stack stores deep copies of the node and edge arrays. Keyboard shortcuts (Ctrl+Z / Ctrl+Y) trigger functions that replace the current state via `setNodes` and `setEdges`, effectively rewinding or replaying the workflow graph without reloading the canvas.

## Key Source Files

- **[`src/areas/workflows/WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/WorkflowsPage.tsx)**: Hosts the main canvas, React Flow provider, drag-and-drop handlers, connection validation, and undo/redo logic.
- **[`src/areas/workflows/nodes/BaseNode.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/BaseNode.tsx)**: Base node component providing common UI elements and handle positioning.
- **[`src/areas/workflows/nodes/ExtensionNode.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/ExtensionNode.tsx)**: Specialized node for user-installed extensions that syncs parameter changes via `useReactFlow`.
- **[`src/areas/workflows/nodes/WhileNode.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/WhileNode.tsx)**: Container node implementation using `NodeResizer` to manage nested workflows.
- **[`src/areas/workflows/nodes/WorkflowEdge.tsx`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/WorkflowEdge.tsx)**: Custom edge component utilizing `getBezierPath` for curved connections.

## Summary

- Modly builds its **visual workflow system** on the `@xyflow/react` package, utilizing `ReactFlowProvider` and `useReactFlow` for context management.
- State is controlled via **useNodesState** and **useEdgesState**, with persistence handled through debounced snapshots in [`WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/WorkflowsPage.tsx).
- **Custom node components** in the `nodes/` directory extend base functionality to support extensions, loops, and 3D asset types.
- **Connection validation** enforces type safety and prevents cycles through the `isValidConnection` callback before edges are committed.
- **History management** leverages immutable state updates via `setNodes` and `setEdges` to enable undo/redo without external state libraries.

## Frequently Asked Questions

### What React Flow hooks does Modly use for state management?

Modly primarily uses `useNodesState` and `useEdgesState` to manage the graph topology, alongside `useReactFlow` to access imperative methods like `screenToFlowPosition`, `updateNodeData`, and `deleteElements`. These hooks are consumed in [`WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/WorkflowsPage.tsx) and individual node components to coordinate state across the canvas.

### How does Modly prevent invalid connections between nodes?

The application implements an `isValidConnection` callback that inspects node types using `getNode` to retrieve source and target definitions. It validates type compatibility between handles and performs a depth-first search to detect potential cycles before allowing the connection to form.

### Can Modly's workflow editor handle nested or container nodes?

Yes. Modly supports container nodes such as the While loop implemented in [`WhileNode.tsx`](https://github.com/lightningpixel/modly/blob/main/WhileNode.tsx). These nodes utilize React Flow’s `NodeResizer` and establish parent-child relationships through the `parentId` property in node data, allowing nested workflows within parent containers.

### How is the undo/redo functionality implemented?

The system maintains a history stack of node and edge array snapshots in [`WorkflowsPage.tsx`](https://github.com/lightningpixel/modly/blob/main/WorkflowsPage.tsx). When users trigger undo or redo shortcuts, the application replaces the current React Flow state by calling `setNodes` and `setEdges` with historical snapshots, enabling non-destructive editing without complex state management libraries.