@easy-nodes/core
v1.0.3
Published
A React-based node graph editor built on [React Flow](https://reactflow.dev). Define nodes declaratively with JSON, wire them together visually, and let the built-in evaluation engine run your graph in topological order. Supports sync and async evaluation
Readme
Graphly
A React-based node graph editor built on React Flow. Define nodes declaratively with JSON, wire them together visually, and let the built-in evaluation engine run your graph in topological order. Supports sync and async evaluation, cycle detection, configurable fields, inline JavaScript operations, a custom operation registry, and partial re-evaluation for performance-sensitive graphs.
Table of Contents
- Installation
- Peer Dependencies
- Quick Start
- Node Definitions
- GraphlyCanvas Props
- Evaluation Engine
- Save and Load
- Store
- Components
- Hooks and Context
- Types Reference
- Re-exported React Flow Utilities
- Context Menu
- Performance Tips
- Full Example
- License
Installation
npm install @easy-nodes/corePeer Dependencies
Graphly requires these peer dependencies. Install them if they are not already in your project:
npm install react react-dom @xyflow/react| Peer Dependency | Version |
| ----------------- | --------- |
| react | ^19.2.4 |
| react-dom | ^19.2.4 |
| @xyflow/react | ^12.10.1|
Graphly imports
@xyflow/react/dist/style.cssinternally, so your bundler must be configured to handle CSS imports (Vite, Next.js, and Create React App handle this out of the box).
Quick Start
import { GraphlyCanvas } from '@easy-nodes/core';
function App() {
const nodeDefinitions = {
number: {
label: 'Number',
category: 'generator',
operationCode: 'return { val: state.config?.val ?? 10 };',
configurable: [
{ id: 'val', label: 'Value', type: 'number', configLocation: 'inline' },
],
outputs: [{ id: 'val', label: 'Value', type: 'number' }],
},
add: {
label: 'Add',
category: 'transformer',
operationCode: 'return { sum: (Number(inp_a) || 0) + (Number(inp_b) || 0) };',
inputs: [
{ id: 'a', label: 'A', type: 'number' },
{ id: 'b', label: 'B', type: 'number' },
],
outputs: [{ id: 'sum', label: 'Sum', type: 'number' }],
},
output: {
label: 'Output',
category: 'output',
isOutput: true,
operationCode: 'return { value: inp_value };',
inputs: [{ id: 'value', label: 'Value', type: 'any' }],
},
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<GraphlyCanvas nodeDefinitions={nodeDefinitions} />
</div>
);
}This renders a full canvas with a draggable node sidebar, a context menu for adding nodes, bezier edges, a minimap, zoom controls, and a background grid. Users can drag nodes from the sidebar, connect them with edges, and the graph evaluates automatically.
Node Definitions
Node definitions are a Record<string, Partial<DynamicNodeData>> object passed to GraphlyCanvas via the nodeDefinitions prop. Each key is a unique identifier for that node type, and the value describes the node's behavior, ports, and UI.
Basic Structure
const nodeDefinitions = {
myNode: {
label: 'My Node', // Display name shown in the node header
category: 'transformer', // Affects header color: 'generator' | 'transformer' | 'output' | 'generic'
inputs: [...], // Array of input ports
outputs: [...], // Array of output ports
configurable: [...], // Array of user-editable config fields
operationCode: '...', // Inline JavaScript string (evaluated via new Function)
operationType: '...', // OR: key into the operations registry
isOutput: false, // Mark as a final output node
settings: { ... }, // Arbitrary metadata (not used by the engine directly)
},
};Inputs and Outputs
Each input and output port is a DynamicNodePort:
interface DynamicNodePort {
id: string; // Unique within the node. Used as handle ID for edge connections.
label: string; // Display label next to the handle.
type?: string; // 'number' | 'string' | 'text' | 'boolean' | 'any' | 'array' | 'object'
options?: string[]; // For dropdown controls: display labels.
values?: any[]; // For dropdown controls: actual values (paired with options by index).
}Input ports appear on the left side of the node. If the port type is number, text, or string, an inline input control is rendered directly on the node so users can type values manually. When an edge is connected to the input, the inline control is disabled and the value comes from the upstream node.
Output ports appear on the right side of the node.
Example with multiple inputs and outputs:
{
"divide": {
"label": "Divide",
"category": "transformer",
"operationCode": "const a = Number(inp_dividend) || 0; const b = Number(inp_divisor) || 1; return { result: a / b, remainder: a % b };",
"inputs": [
{ "id": "dividend", "label": "Dividend", "type": "number" },
{ "id": "divisor", "label": "Divisor", "type": "number" }
],
"outputs": [
{ "id": "result", "label": "Result", "type": "number" },
{ "id": "remainder", "label": "Remainder", "type": "number" }
]
}
}Categories
The category field determines the header color of the node:
| Category | Color |
| ------------- | ------------ |
| generator | Dark Green |
| transformer | Indigo |
| output | Dark Red |
| generic | Dark Gray |
If omitted, defaults to generic.
Operation Code (Inline JavaScript)
operationCode is a JavaScript string evaluated with new Function(). The function receives these named arguments:
| Argument pattern | Source | Example |
| --- | --- | --- |
| inp_<inputId> | Resolved value of each input port | inp_a, inp_value |
| conf_<configId> | Current value of each configurable field | conf_gravityConstant |
| state | The node's full internal state object | state.outputs, state.config |
The function must return an object whose keys match the output port IDs:
// For a node with inputs: [{ id: 'a' }, { id: 'b' }] and outputs: [{ id: 'sum' }]
"return { sum: (Number(inp_a) || 0) + (Number(inp_b) || 0) };"For generator nodes with no inputs, use state.config to read user-editable values:
// For a node with configurable: [{ id: 'val' }] and outputs: [{ id: 'val' }]
"return { val: state.config?.val ?? 10 };"The compiled function is cached internally, so repeated evaluations of the same operationCode do not recompile.
Operation Type (Registry)
Instead of inline code, you can reference a function from an external registry by setting operationType:
// Node definition
const defs = {
fetchUrl: {
label: 'Fetch URL',
category: 'transformer',
operationType: 'fetchUrl', // key into the registry
inputs: [{ id: 'url', label: 'URL', type: 'string' }],
outputs: [{ id: 'data', label: 'Data', type: 'any' }],
},
};
// Operation registry
const operations = {
fetchUrl: async (inputs, state) => {
const res = await fetch(inputs.url || '');
const data = await res.json();
return { data };
},
};
<GraphlyCanvas
nodeDefinitions={defs}
operations={operations}
evaluationMode="async"
/>Registry operations receive (inputs: Record<string, any>, state: Record<string, any>) and return Record<string, any> (or a Promise of one in async mode).
If both
operationCodeandoperationTypeare set,operationCodetakes priority.
Configurable Fields
Configurable fields let users edit values that are not connected via edges. They are stored in state.config and accessible in operationCode as conf_<id>.
interface ConfigurableField {
id: string;
label: string;
type?: string; // 'number' | 'string' | 'text' | 'boolean' | dropdown (when options is set)
options?: string[]; // Display labels for a dropdown
values?: any[]; // Actual stored values (paired with options by index)
configLocation?: 'inline' | 'panel' | 'both'; // Default: 'panel'
}configLocation controls where the control appears:
| Value | Inline on node | In config panel |
| -------- | -------------- | --------------- |
| inline | Yes | No |
| panel | No | Yes (default) |
| both | Yes | Yes |
Dropdown example with separate display labels and stored values:
{
"dropdown_input": {
"label": "Mode Selector",
"category": "generator",
"operationCode": "return { selected: state.config?.selected ?? 1 };",
"configurable": [
{
"id": "selected",
"label": "Mode",
"type": "string",
"options": ["Option 1", "Option 2", "Option 3"],
"values": [1, 2, 3],
"configLocation": "inline"
}
],
"outputs": [{ "id": "selected", "label": "Selected", "type": "number" }]
}
}When options is provided without values, the option label string is stored directly. When values is provided, the value at the matching index is stored instead.
Output Nodes
Set isOutput: true to mark a node as a terminal output. This is a semantic marker your application can use to identify which nodes produce final results. The engine does not treat them differently, but the demo app uses it to render an "App Outputs" panel.
{
"output_display": {
"label": "Output Display",
"isOutput": true,
"category": "output",
"operationCode": "return { value: inp_value };",
"inputs": [{ "id": "value", "label": "Value", "type": "any" }]
}
}GraphlyCanvas Props
GraphlyCanvas extends all React Flow props (except onLoad) and adds:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| nodeDefinitions | Record<string, Partial<DynamicNodeData>> | {} | Node type definitions. Populates the sidebar and context menu. |
| operations | OperationRegistry | {} | Map of operationType keys to functions. |
| evaluationMode | 'sync' \| 'async' | 'sync' | When 'async', registry operations that return Promises are awaited. |
| evaluationOptions | EvaluateGraphOptions | — | Callbacks for diagnostics: onNodeEvaluated, onError, onCycleDetected. |
| onSave | (data: GraphData) => Promise<void> \| void | — | Called when the user presses Cmd/Ctrl+S or calls ref.save(). |
| onLoad | () => Promise<GraphData \| null> \| GraphData \| null | — | Called on mount to load initial graph data. |
| showControls | boolean | true | Show React Flow zoom/fit controls. |
| showMiniMap | boolean | true | Show the React Flow minimap. |
| showBackground | boolean | true | Show the dot grid background. |
| nodeTypes | Record<string, ComponentType> | — | Additional custom React Flow node types (merged with the built-in dynamicNode). |
| children | ReactNode | — | Extra children rendered inside the React Flow canvas (e.g., custom <Panel>). |
| ref | Ref<GraphlyCanvasHandle> | — | Imperative handle for save/load/getGraphData. |
All other React Flow props (e.g., fitView, minZoom, maxZoom, panOnDrag, defaultViewport, etc.) are forwarded directly to <ReactFlow>.
Evaluation Engine
The engine evaluates nodes in dependency order using Kahn's algorithm (topological sort). For each node it:
- Gathers inputs from connected upstream node outputs and from
state.inputs(manual/default values). - Runs the node's operation (
operationCodeoroperationTypefrom the registry). - Writes
state.inputs,state.outputs, andstate.engineMetaback to the node.
The graph re-evaluates automatically whenever edges change, a new node is added, or a configurable field value changes.
Sync Evaluation (Default)
import { evaluateGraph } from '@easy-nodes/core';
// Used internally by GraphlyCanvas; also available for standalone use:
const evaluatedNodes = evaluateGraph(nodes, edges, registry, options);All operations must return Record<string, any> synchronously. If an operation returns a Promise in sync mode, the engine attaches an error: "This node requires async evaluation mode.".
Async Evaluation
Enable by setting evaluationMode="async":
<GraphlyCanvas
evaluationMode="async"
operations={operations}
nodeDefinitions={defs}
/>Registry operations may return Promise<Record<string, any>>:
const operations = {
fetchUrl: async (inputs) => {
const res = await fetch(inputs.url || '');
const data = await res.json();
return { data };
},
};The standalone function is also available:
import { evaluateGraphAsync } from '@easy-nodes/core';
const evaluatedNodes = await evaluateGraphAsync(nodes, edges, registry, options);Two-Phase Results
For operations that have both an immediate synchronous result and a deferred async result, return a two-phase object:
const operations = {
fetchWithStatus: (inputs) => ({
immediate: { loading: true },
promise: fetch(inputs.url)
.then(res => res.json())
.then(data => ({ loading: false, data })),
}),
};The immediate outputs are applied right away (and can unblock downstream nodes), while promise outputs are merged in when resolved. This only works in async evaluation mode.
Partial Re-evaluation
When only part of the graph changes, you can re-evaluate just the affected subgraph:
import { evaluateSubgraph } from '@easy-nodes/core';
const evaluatedNodes = evaluateSubgraph(nodes, edges, registry, {
startNodeIds: ['node-1', 'node-2'],
onNodeEvaluated: (id, { outputs }) => console.log(id, outputs),
});Only startNodeIds and their downstream dependents are re-evaluated. All other nodes retain their existing state.outputs.
Cycle Detection
The engine automatically detects cycles. Nodes involved in a cycle receive:
state.engineMeta.error = { message: 'Cycle detected' }state.engineMeta.cyclesInvolving = ['node-A', 'node-B', ...]
The DynamicNode component renders a red error banner on nodes with errors.
Engine Metadata
Each evaluated node gets state.engineMeta (type EngineMeta):
interface EngineMeta {
lastEvaluatedAt?: number; // Timestamp of last evaluation
error?: {
message: string;
stackSnippet?: string; // First 3 lines of the stack trace
};
cyclesInvolving?: string[]; // IDs of all nodes in the detected cycle
}Evaluation Options (Diagnostics)
Pass evaluationOptions to receive callbacks during evaluation:
<GraphlyCanvas
evaluationOptions={{
onNodeEvaluated: (nodeId, { inputs, outputs }) => {
console.log(`Node ${nodeId} evaluated:`, outputs);
},
onError: (nodeId, error) => {
console.error(`Node ${nodeId} failed:`, error);
},
onCycleDetected: (cycleNodeIds) => {
console.warn('Cycle detected:', cycleNodeIds);
},
}}
nodeDefinitions={defs}
/>Save and Load
Using the Ref Handle
GraphlyCanvas exposes an imperative handle via React.forwardRef:
import { useRef } from 'react';
import { GraphlyCanvas, GraphlyCanvasHandle, GraphData } from '@easy-nodes/core';
function App() {
const canvasRef = useRef<GraphlyCanvasHandle>(null);
const handleSaveClick = () => {
canvasRef.current?.save(); // triggers onSave callback
};
const handleExport = () => {
const data: GraphData = canvasRef.current!.getGraphData();
// data contains { nodes, edges, viewport }
console.log(JSON.stringify(data));
};
const handleImport = (data: GraphData) => {
canvasRef.current?.loadGraphData(data);
};
return (
<GraphlyCanvas
ref={canvasRef}
onSave={async (data) => {
localStorage.setItem('my-graph', JSON.stringify(data));
}}
onLoad={() => {
const raw = localStorage.getItem('my-graph');
return raw ? JSON.parse(raw) : null;
}}
nodeDefinitions={defs}
/>
);
}GraphlyCanvasHandle methods:
| Method | Description |
| --- | --- |
| save(): Promise<void> | Calls onSave with current { nodes, edges, viewport }. Sets isSaving and clears hasUnsavedChanges. |
| getGraphData(): GraphData | Returns current { nodes, edges, viewport } without triggering save. |
| loadGraphData(data: GraphData): void | Replaces nodes, edges, and viewport. Resets hasUnsavedChanges. |
onSave / onLoad Callbacks
| Callback | Signature | When |
| --- | --- | --- |
| onSave | (data: GraphData) => Promise<void> \| void | Called by ref.save() or Cmd/Ctrl+S. |
| onLoad | () => Promise<GraphData \| null> \| GraphData \| null | Called once on initial mount. Return null for an empty canvas. |
GraphlyProjectFile
A convenience type for consumers who store metadata alongside the graph:
type GraphlyProjectFile = {
version?: number;
name?: string;
updatedAt?: string; // ISO date string
graph: GraphData;
};The library does not perform any I/O itself. This type is provided for consistency if you build project management features.
Keyboard Shortcut
Cmd+S (Mac) / Ctrl+S (Windows/Linux) triggers onSave automatically.
Store
Graphly uses a Zustand store internally. You can access it directly for advanced use cases:
import { useGraphlyStore } from '@easy-nodes/core';
function MyComponent() {
const nodes = useGraphlyStore((s) => s.nodes);
const edges = useGraphlyStore((s) => s.edges);
const hasUnsavedChanges = useGraphlyStore((s) => s.hasUnsavedChanges);
const updateNodeData = useGraphlyStore((s) => s.updateNodeData);
// ...
}GraphlyStoreState fields and methods:
| Field / Method | Type | Description |
| --- | --- | --- |
| nodes | Node[] | Current React Flow nodes. |
| edges | Edge[] | Current React Flow edges. |
| onNodesChange | OnNodesChange | Handler for React Flow node changes (position, selection, removal). |
| onEdgesChange | OnEdgesChange | Handler for React Flow edge changes. |
| onConnect | OnConnect | Handler for new connections. Automatically replaces existing edges on the same input handle. |
| setNodes(nodes) | (Node[]) => void | Replace all nodes. |
| setEdges(edges) | (Edge[]) => void | Replace all edges. |
| addNode(node) | (Node) => void | Append a single node. |
| updateNodeData(id, patch) | (string, Partial<DynamicNodeData>) => void | Shallow-merge a data patch into a specific node. |
| configPanelNodeId | string \| null | ID of the node whose config panel is open, or null. |
| setConfigPanelNodeId(id) | (string \| null) => void | Open/close the config panel for a node. |
| isSaving | boolean | True while a save operation is in progress. |
| setIsSaving(v) | (boolean) => void | — |
| hasUnsavedChanges | boolean | True when the graph has been modified since the last save/load. |
| setHasUnsavedChanges(v) | (boolean) => void | — |
The store enforces single-connection-per-input-handle: when a new edge connects to a target handle, any existing edge on that same target+targetHandle is removed.
Components
GraphlyCanvas
The main component. Renders the full editor: React Flow canvas, node sidebar, config panel, context menu, minimap, controls, and background. See GraphlyCanvas Props for the full API.
DynamicNode
The built-in node component registered as dynamicNode. Renders:
- A colored header based on
category. - Input handles on the left with optional inline controls (for
number/text/stringtypes). - Output handles on the right.
- Inline configurable fields (when
configLocationis'inline'or'both'). - A gear button to open the config panel.
- An error banner when
state.engineMeta.erroris present.
You do not need to use or import DynamicNode directly; it is automatically registered as a node type.
NodeSidebar
A draggable panel (top-left) listing all node definitions. Users drag items from the sidebar onto the canvas to create nodes. Only renders when at least one definition is provided.
NodeConfigPanel
A panel (top-right) that opens when the user clicks the gear icon on a node or selects "Open config panel" from the context menu. Displays all configurable fields whose configLocation is 'panel' or 'both' (or unset, since 'panel' is the default). Close with Escape or the Close button.
NodeConfigControls / PortControl
PortControl renders the appropriate HTML control for a port or configurable field based on its type:
| Port type | Control |
| --- | --- |
| number | <input type="number"> |
| string / text | <input type="text"> |
| boolean | <input type="checkbox"> |
| Has options | <select> dropdown |
| Anything else | <input type="text"> |
Props:
interface PortControlProps {
port: PortOrConfigurable; // { id, label, type?, options?, values? }
value: unknown;
onChange: (value: unknown) => void;
compact?: boolean; // Smaller styling for inline node UI
disabled?: boolean; // Disabled when an edge is connected to the input
className?: string;
}DeletableEdge
A bezier edge used as the default edge type. All edges are rendered as bezier curves. Edges can be deleted via the context menu or by selecting and pressing Delete.
CanvasContextMenu
A right-click context menu rendered as a portal. Behavior depends on where you right-click:
Pane (canvas background):
- Add node (submenu listing all definitions)
- Paste (if clipboard has content)
- Delete selection (if nodes are selected)
Node:
- Open config panel
- Delete
- Copy
- Cut
Hooks and Context
useGraphlyData
A hook for data loading and saving, used internally by GraphlyCanvas:
import { useGraphlyData } from '@easy-nodes/core';
const { save, isSaving, hasUnsavedChanges, nodes, edges } = useGraphlyData({
onSave: async (data) => { /* persist data */ },
onLoad: async () => { /* return GraphData or null */ },
});useGraphlyConfig / GraphlyConfigContext
GraphlyConfigContext provides the onConfigChange callback used by DynamicNode and NodeConfigPanel to propagate configurable field changes. GraphlyCanvas sets this up automatically.
import { useGraphlyConfig } from '@easy-nodes/core';
const config = useGraphlyConfig();
// config?.onConfigChange(nodeId, fieldId, newValue)Types Reference
All types are exported from the package root:
import type {
GraphData,
GraphlyProjectFile,
GraphlyCanvasHandle,
GraphlyStoreState,
SyncCallbacks,
DynamicNodePort,
ConfigurableField,
NodeCategory,
EngineMeta,
DynamicNodeData,
DynamicGraphlyNode,
NodeOperation,
OperationRegistry,
EvaluateGraphOptions,
EvaluateSubgraphOptions,
} from '@easy-nodes/core';Key types at a glance
type GraphData = {
nodes: Node[];
edges: Edge[];
viewport?: Viewport;
};
type NodeCategory = 'generator' | 'transformer' | 'output' | 'generic';
interface DynamicNodeData extends Record<string, unknown> {
label: string;
inputs?: DynamicNodePort[];
outputs?: DynamicNodePort[];
configurable?: ConfigurableField[];
settings?: Record<string, any>;
category?: NodeCategory;
isOutput?: boolean;
operationType?: string;
operationCode?: string;
state?: Record<string, any>; // { inputs, outputs, config, engineMeta }
}
type DynamicGraphlyNode = Node<DynamicNodeData>;
type NodeOperation = (
inputs: Record<string, any>,
state: Record<string, any>
) => Record<string, any> | Promise<Record<string, any>>;
type OperationRegistry = Record<string, NodeOperation>;
interface EvaluateGraphOptions {
onNodeEvaluated?: (nodeId: string, payload: { inputs: Record<string, any>; outputs: Record<string, any> }) => void;
onError?: (nodeId: string, error: unknown) => void;
onCycleDetected?: (cycleGroupNodeIds: string[]) => void;
}
interface EvaluateSubgraphOptions extends EvaluateGraphOptions {
startNodeIds: string[];
}Re-exported React Flow Utilities
For convenience, Graphly re-exports commonly needed items from @xyflow/react so you don't need to install or import it separately for basic usage:
import {
Handle,
Position,
useReactFlow,
ReactFlowProvider,
applyNodeChanges,
applyEdgeChanges,
addEdge,
MarkerType,
} from '@easy-nodes/core';
import type { Node, Edge } from '@easy-nodes/core';Context Menu
Right-click anywhere on the canvas or on a node to open the context menu.
Canvas right-click:
- Add node — opens a submenu with all available node definitions. The new node is placed at the click position.
- Paste — paste previously copied nodes (and their internal edges) at the click position.
- Delete selection — removes all currently selected nodes and their connected edges.
Node right-click:
- Open config panel — opens the config panel for this node.
- Delete — removes the node and its edges.
- Copy — copies the node (or all selected nodes) to the clipboard.
- Cut — copies and then removes the node(s).
Copy/paste duplicates nodes with new IDs and repositions them relative to the paste location.
Performance Tips
State comparison: The canvas uses
hasStateChanged(oldState, newState)to compare onlyinputs,outputs, andengineMeta.error/cyclesInvolvingbefore updating the store. This avoids unnecessary re-renders whenlastEvaluatedAtchanges but nothing else does.Partial re-evaluation: If you know which nodes changed (e.g., after an edge or input edit), call
evaluateSubgraphwithstartNodeIdsand update the store with the returned nodes instead of re-evaluating the full graph.Async mode: Only use
evaluationMode="async"when you have operations that return Promises. Sync evaluation has less overhead.Operation caching: Inline
operationCodestrings are compiled once and cached in aMap<string, Function>, so repeated evaluations don't pay thenew Function()cost.
Full Example
A complete example with save/load, async operations, diagnostics, and a custom registry operation:
import { useRef, useState, useMemo, useCallback } from 'react';
import {
GraphlyCanvas,
GraphlyCanvasHandle,
GraphData,
DynamicNodeData,
EvaluateGraphOptions,
OperationRegistry,
} from '@easy-nodes/core';
const nodeDefinitions: Record<string, Partial<DynamicNodeData>> = {
number: {
label: 'Number',
category: 'generator',
operationCode: 'return { val: state.config?.val ?? 0 };',
configurable: [
{ id: 'val', label: 'Value', type: 'number', configLocation: 'inline' },
],
outputs: [{ id: 'val', label: 'Value', type: 'number' }],
},
multiply: {
label: 'Multiply',
category: 'transformer',
operationCode:
'return { product: (Number(inp_a) || 0) * (Number(inp_b) || 0) };',
inputs: [
{ id: 'a', label: 'A', type: 'number' },
{ id: 'b', label: 'B', type: 'number' },
],
outputs: [{ id: 'product', label: 'Product', type: 'number' }],
},
fetchJson: {
label: 'Fetch JSON',
category: 'transformer',
operationType: 'fetchJson',
inputs: [{ id: 'url', label: 'URL', type: 'string' }],
outputs: [{ id: 'data', label: 'Data', type: 'any' }],
},
output: {
label: 'Result',
category: 'output',
isOutput: true,
operationCode: 'return { value: inp_value };',
inputs: [{ id: 'value', label: 'Value', type: 'any' }],
},
};
const operations: OperationRegistry = {
fetchJson: async (inputs) => {
const res = await fetch(inputs.url || 'https://jsonplaceholder.typicode.com/todos/1');
return { data: await res.json() };
},
};
function App() {
const canvasRef = useRef<GraphlyCanvasHandle>(null);
const [mode, setMode] = useState<'sync' | 'async'>('async');
const evalOpts: EvaluateGraphOptions = useMemo(() => ({
onNodeEvaluated: (id, { outputs }) => console.log(`[${id}]`, outputs),
onError: (id, err) => console.error(`[${id}] Error:`, err),
onCycleDetected: (ids) => console.warn('Cycle:', ids),
}), []);
const handleSave = useCallback(async (data: GraphData) => {
localStorage.setItem('my-graph', JSON.stringify(data));
}, []);
const handleLoad = useCallback((): GraphData | null => {
const raw = localStorage.getItem('my-graph');
return raw ? JSON.parse(raw) : null;
}, []);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<GraphlyCanvas
ref={canvasRef}
nodeDefinitions={nodeDefinitions}
operations={operations}
evaluationMode={mode}
evaluationOptions={evalOpts}
onSave={handleSave}
onLoad={handleLoad}
/>
</div>
);
}
export default App;License
ISC
