@infinit-canvas/react 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -1039
- package/dist/react-infinite-canvas.cjs +438 -419
- package/dist/react-infinite-canvas.js +2659 -2551
- package/package.json +4 -2
- package/src/types.d.ts +7 -0
package/README.md
CHANGED
|
@@ -1,99 +1,14 @@
|
|
|
1
1
|
# @infinit-canvas/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@infinit-canvas/react)
|
|
4
|
+
[](https://github.com/awaisshah228/infinit-canvas/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
**Drop-in
|
|
6
|
+
A high-performance infinite canvas React component powered by **OffscreenCanvas + Web Workers**. Drop-in React Flow compatible API — but renders on a canvas for **10x better performance** at scale.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
**5,000+ nodes at 60fps** vs React Flow's ~500 before lag.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- [Key Concepts (Glossary)](#key-concepts-glossary)
|
|
12
|
-
- [Install](#install)
|
|
13
|
-
- [Quick Start](#quick-start)
|
|
14
|
-
- [Nodes](#nodes)
|
|
15
|
-
- [Edges](#edges)
|
|
16
|
-
- [Handles](#handles)
|
|
17
|
-
- [Custom Nodes](#custom-nodes)
|
|
18
|
-
- [Custom Edges](#custom-edges)
|
|
19
|
-
- [State Management](#state-management)
|
|
20
|
-
- [Hooks](#hooks)
|
|
21
|
-
- [Viewport & Camera](#viewport--camera)
|
|
22
|
-
- [Interactions](#interactions)
|
|
23
|
-
- [Styling & Appearance](#styling--appearance)
|
|
24
|
-
- [Components](#components)
|
|
25
|
-
- [Utilities](#utilities)
|
|
26
|
-
- [Performance](#performance)
|
|
27
|
-
- [Full Props Reference](#full-props-reference)
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Key Concepts (Glossary)
|
|
32
|
-
|
|
33
|
-
If you're new to canvas/graph libraries, here's what each term means:
|
|
34
|
-
|
|
35
|
-
### Canvas
|
|
36
|
-
|
|
37
|
-
The drawing surface. Think of it as an infinite whiteboard you can pan around and zoom into. Under the hood, this uses an HTML `<canvas>` element transferred to a **Web Worker** so all drawing happens off the main thread.
|
|
38
|
-
|
|
39
|
-
### Node
|
|
40
|
-
|
|
41
|
-
A box/card on the canvas. Each node has a position (x, y), dimensions (width, height), and data (any info you want to store). Nodes are the "things" on your canvas — could be workflow steps, diagram boxes, sticky notes, etc.
|
|
42
|
-
|
|
43
|
-
### Edge
|
|
44
|
-
|
|
45
|
-
A line connecting two nodes. An edge goes **from** a source node **to** a target node. Edges can be straight lines, curves (bezier), or right-angle paths (smoothstep/step).
|
|
46
|
-
|
|
47
|
-
### Handle
|
|
48
|
-
|
|
49
|
-
A connection point on a node. By default every node has two handles: a **source** handle (right side, where edges come out) and a **target** handle (left side, where edges go in). You can customize handle positions and add multiple handles.
|
|
50
|
-
|
|
51
|
-
### Viewport / Camera
|
|
52
|
-
|
|
53
|
-
The visible area of the canvas. When you pan or zoom, you're moving the camera. The viewport is defined by three values: `x` (horizontal offset), `y` (vertical offset), and `zoom` (scale level).
|
|
54
|
-
|
|
55
|
-
### World Space vs Screen Space
|
|
56
|
-
|
|
57
|
-
- **World space**: The coordinate system of the canvas. A node at position `{x: 500, y: 300}` is always at that world position, no matter where you've panned or zoomed.
|
|
58
|
-
- **Screen space**: Pixel coordinates on your monitor. Where something appears on screen depends on the camera position and zoom level.
|
|
59
|
-
|
|
60
|
-
### Panning
|
|
61
|
-
|
|
62
|
-
Click-and-drag on empty canvas space to move around. The camera position changes, but nodes stay at their world positions.
|
|
63
|
-
|
|
64
|
-
### Zooming
|
|
65
|
-
|
|
66
|
-
Scroll wheel, pinch gesture, or double-click to zoom in/out. Zooming scales everything relative to the cursor position.
|
|
67
|
-
|
|
68
|
-
### Frustum Culling
|
|
69
|
-
|
|
70
|
-
A performance technique: only draw what's visible on screen. If you have 5000 nodes but only 50 are visible in the viewport, only those 50 get rendered. The canvas uses a **spatial grid** to find visible nodes in O(1) time instead of checking all 5000.
|
|
71
|
-
|
|
72
|
-
### Web Worker / OffscreenCanvas
|
|
73
|
-
|
|
74
|
-
JavaScript normally runs on a single thread — the "main thread." Heavy canvas drawing on the main thread blocks React updates and user input. This library moves ALL canvas rendering to a **Web Worker** (a separate thread) using **OffscreenCanvas**, so your UI stays responsive.
|
|
75
|
-
|
|
76
|
-
### Level of Detail (LOD)
|
|
77
|
-
|
|
78
|
-
When zoomed far out, small details like text labels and shadows are invisible anyway. LOD skips rendering these at low zoom levels to save drawing time.
|
|
79
|
-
|
|
80
|
-
### Spatial Grid / Spatial Index
|
|
81
|
-
|
|
82
|
-
A data structure that divides the world into grid cells. Each cell knows which nodes are inside it. To find visible nodes, you only check cells that overlap the viewport — much faster than checking every node individually.
|
|
83
|
-
|
|
84
|
-
### Edge Routing
|
|
85
|
-
|
|
86
|
-
Smart edge paths that go around obstacles. Instead of drawing a straight line through other nodes, the edge routing system computes right-angle paths that avoid overlapping with nodes.
|
|
87
|
-
|
|
88
|
-
### Batching
|
|
89
|
-
|
|
90
|
-
Instead of drawing each node/edge one at a time (thousands of draw calls), the renderer groups them and draws all at once (3-10 draw calls total). Fewer draw calls = faster rendering.
|
|
91
|
-
|
|
92
|
-
### Zustand Store
|
|
93
|
-
|
|
94
|
-
A lightweight state manager used internally. When you wrap your app in `<InfiniteCanvasProvider>`, it creates a Zustand store that hooks like `useNodes()`, `useStore()`, and `useReactFlow()` subscribe to.
|
|
95
|
-
|
|
96
|
-
---
|
|
10
|
+
**npm:** [https://www.npmjs.com/package/@infinit-canvas/react](https://www.npmjs.com/package/@infinit-canvas/react)
|
|
11
|
+
**GitHub:** [https://github.com/awaisshah228/infinit-canvas](https://github.com/awaisshah228/infinit-canvas)
|
|
97
12
|
|
|
98
13
|
## Install
|
|
99
14
|
|
|
@@ -101,91 +16,25 @@ A lightweight state manager used internally. When you wrap your app in `<Infinit
|
|
|
101
16
|
npm install @infinit-canvas/react
|
|
102
17
|
```
|
|
103
18
|
|
|
104
|
-
---
|
|
105
|
-
|
|
106
19
|
## Quick Start
|
|
107
20
|
|
|
108
|
-
### Simplest example — just nodes and edges
|
|
109
|
-
|
|
110
21
|
```jsx
|
|
111
|
-
import {
|
|
112
|
-
import {
|
|
113
|
-
InfiniteCanvas,
|
|
114
|
-
applyNodeChanges,
|
|
115
|
-
applyEdgeChanges,
|
|
116
|
-
addEdge,
|
|
117
|
-
} from '@infinit-canvas/react';
|
|
22
|
+
import { InfiniteCanvas, useNodesState, useEdgesState, addEdge, Controls, MiniMap, Background } from '@infinit-canvas/react';
|
|
118
23
|
import '@infinit-canvas/react/styles.css';
|
|
119
24
|
|
|
120
|
-
// Define your nodes — each needs a unique id and a position
|
|
121
25
|
const initialNodes = [
|
|
122
|
-
{ id: '1', position: { x: 0, y: 0 }, data: { label: '
|
|
123
|
-
{ id: '2', position: { x:
|
|
26
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } },
|
|
27
|
+
{ id: '2', position: { x: 250, y: 100 }, data: { label: 'Node 2' } },
|
|
124
28
|
];
|
|
125
29
|
|
|
126
|
-
// Define your edges — each connects a source node to a target node
|
|
127
30
|
const initialEdges = [
|
|
128
31
|
{ id: 'e1-2', source: '1', target: '2' },
|
|
129
32
|
];
|
|
130
33
|
|
|
131
34
|
function App() {
|
|
132
|
-
const [nodes, setNodes] = useState(initialNodes);
|
|
133
|
-
const [edges, setEdges] = useState(initialEdges);
|
|
134
|
-
|
|
135
|
-
// Called when nodes are dragged, selected, or removed
|
|
136
|
-
const onNodesChange = useCallback(
|
|
137
|
-
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
|
138
|
-
[]
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// Called when edges are selected or removed
|
|
142
|
-
const onEdgesChange = useCallback(
|
|
143
|
-
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
|
144
|
-
[]
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Called when user drags from one handle to another
|
|
148
|
-
const onConnect = useCallback(
|
|
149
|
-
(connection) => setEdges((eds) => addEdge(connection, eds)),
|
|
150
|
-
[]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<InfiniteCanvas
|
|
155
|
-
nodes={nodes}
|
|
156
|
-
edges={edges}
|
|
157
|
-
onNodesChange={onNodesChange}
|
|
158
|
-
onEdgesChange={onEdgesChange}
|
|
159
|
-
onConnect={onConnect}
|
|
160
|
-
fitView // auto-zoom to fit all nodes on mount
|
|
161
|
-
height="100vh"
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Even simpler — with helper hooks
|
|
168
|
-
|
|
169
|
-
```jsx
|
|
170
|
-
import {
|
|
171
|
-
InfiniteCanvas,
|
|
172
|
-
useNodesState,
|
|
173
|
-
useEdgesState,
|
|
174
|
-
addEdge,
|
|
175
|
-
} from '@infinit-canvas/react';
|
|
176
|
-
import '@infinit-canvas/react/styles.css';
|
|
177
|
-
|
|
178
|
-
const initialNodes = [
|
|
179
|
-
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Hello' } },
|
|
180
|
-
{ id: '2', position: { x: 250, y: 100 }, data: { label: 'World' } },
|
|
181
|
-
];
|
|
182
|
-
|
|
183
|
-
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
|
|
184
|
-
|
|
185
|
-
function App() {
|
|
186
|
-
// useNodesState/useEdgesState handle the onChange boilerplate for you
|
|
187
35
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
188
36
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
37
|
+
const onConnect = (conn) => setEdges((eds) => addEdge(conn, eds));
|
|
189
38
|
|
|
190
39
|
return (
|
|
191
40
|
<InfiniteCanvas
|
|
@@ -193,907 +42,123 @@ function App() {
|
|
|
193
42
|
edges={edges}
|
|
194
43
|
onNodesChange={onNodesChange}
|
|
195
44
|
onEdgesChange={onEdgesChange}
|
|
196
|
-
onConnect={
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## Nodes
|
|
207
|
-
|
|
208
|
-
A node represents an item on the canvas.
|
|
209
|
-
|
|
210
|
-
```js
|
|
211
|
-
{
|
|
212
|
-
id: 'node-1', // Required. Unique identifier.
|
|
213
|
-
position: { x: 100, y: 50 }, // Required. World-space coordinates.
|
|
214
|
-
data: { label: 'My Node' }, // Required. Your custom data — passed to custom node components.
|
|
215
|
-
type: 'default', // Optional. Which node component to render (see Custom Nodes).
|
|
216
|
-
width: 160, // Optional. Width in pixels (default: 160).
|
|
217
|
-
height: 60, // Optional. Height in pixels (default: 60).
|
|
218
|
-
selected: false, // Optional. Is this node selected?
|
|
219
|
-
hidden: false, // Optional. Hide this node entirely.
|
|
220
|
-
draggable: true, // Optional. Can the user drag this node?
|
|
221
|
-
parentId: 'group-1', // Optional. Makes this a child of another node (sub-flows).
|
|
222
|
-
handles: [ // Optional. Custom connection handles.
|
|
223
|
-
{ type: 'source', position: 'bottom' },
|
|
224
|
-
{ type: 'target', position: 'top' },
|
|
225
|
-
],
|
|
226
|
-
style: { background: 'red' }, // Optional. CSS styles (only for custom-rendered nodes).
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Node Types
|
|
231
|
-
|
|
232
|
-
- **No type / `'default'`** — Rendered by the canvas worker (fast, drawn as a rounded rectangle with label)
|
|
233
|
-
- **`'input'`** — Built-in node with only a source handle (output only)
|
|
234
|
-
- **`'output'`** — Built-in node with only a target handle (input only)
|
|
235
|
-
- **`'group'`** — Container node that can hold child nodes (set `parentId` on children)
|
|
236
|
-
- **Custom type** — Your own React component (see [Custom Nodes](#custom-nodes))
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## Edges
|
|
241
|
-
|
|
242
|
-
An edge is a visual connection between two nodes.
|
|
243
|
-
|
|
244
|
-
```js
|
|
245
|
-
{
|
|
246
|
-
id: 'edge-1', // Required. Unique identifier.
|
|
247
|
-
source: 'node-1', // Required. ID of the source node (where the edge starts).
|
|
248
|
-
target: 'node-2', // Required. ID of the target node (where the edge ends).
|
|
249
|
-
type: 'default', // Optional. Edge style: 'default' | 'straight' | 'smoothstep' | 'step' | 'bezier'
|
|
250
|
-
label: 'connects', // Optional. Text label shown at the edge midpoint.
|
|
251
|
-
animated: true, // Optional. Dashed line animation (marching ants).
|
|
252
|
-
selected: false, // Optional. Is this edge selected?
|
|
253
|
-
sourceHandle: 'a', // Optional. Which handle on the source node (if it has multiple).
|
|
254
|
-
targetHandle: 'b', // Optional. Which handle on the target node.
|
|
255
|
-
style: {}, // Optional. CSS styles (only for custom-rendered edges).
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Edge Types
|
|
260
|
-
|
|
261
|
-
| Type | Description |
|
|
262
|
-
|------|-------------|
|
|
263
|
-
| `'default'` / `'bezier'` | Smooth curved line (cubic bezier). Good for most use cases. |
|
|
264
|
-
| `'straight'` | Direct straight line between handles. |
|
|
265
|
-
| `'smoothstep'` | Right-angle path with rounded corners. Great for flowcharts. Supports edge routing (obstacle avoidance). |
|
|
266
|
-
| `'step'` | Right-angle path with sharp corners. Like smoothstep but without the rounded bends. |
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## Handles
|
|
271
|
-
|
|
272
|
-
Handles are the connection points on nodes — the dots you drag from/to to create edges.
|
|
273
|
-
|
|
274
|
-
```jsx
|
|
275
|
-
import { Handle, Position } from '@infinit-canvas/react';
|
|
276
|
-
|
|
277
|
-
function MyNode({ data }) {
|
|
278
|
-
return (
|
|
279
|
-
<div style={{ padding: 10, border: '1px solid #ccc' }}>
|
|
280
|
-
{/* Target handle on top — edges can connect TO this */}
|
|
281
|
-
<Handle type="target" position={Position.Top} />
|
|
282
|
-
|
|
283
|
-
<span>{data.label}</span>
|
|
284
|
-
|
|
285
|
-
{/* Source handle on bottom — edges can start FROM this */}
|
|
286
|
-
<Handle type="source" position={Position.Bottom} />
|
|
287
|
-
</div>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Handle positions
|
|
293
|
-
|
|
294
|
-
- `Position.Top` — Top center of the node
|
|
295
|
-
- `Position.Bottom` — Bottom center
|
|
296
|
-
- `Position.Left` — Left center
|
|
297
|
-
- `Position.Right` — Right center (default for source handles)
|
|
298
|
-
|
|
299
|
-
### Multiple handles
|
|
300
|
-
|
|
301
|
-
Add an `id` to distinguish them:
|
|
302
|
-
|
|
303
|
-
```jsx
|
|
304
|
-
<Handle type="source" position={Position.Right} id="output-a" />
|
|
305
|
-
<Handle type="source" position={Position.Bottom} id="output-b" />
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Then in edges: `{ source: 'node-1', sourceHandle: 'output-a', target: 'node-2' }`
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## Custom Nodes
|
|
313
|
-
|
|
314
|
-
By default, nodes are drawn by the canvas worker (fast rectangles with text). For rich UI (buttons, inputs, images), register a custom React component:
|
|
315
|
-
|
|
316
|
-
```jsx
|
|
317
|
-
// 1. Define your component — receives node props
|
|
318
|
-
function ColorPickerNode({ data, selected }) {
|
|
319
|
-
return (
|
|
320
|
-
<div style={{
|
|
321
|
-
padding: 10,
|
|
322
|
-
background: data.color,
|
|
323
|
-
border: selected ? '2px solid blue' : '1px solid #ccc',
|
|
324
|
-
borderRadius: 8,
|
|
325
|
-
}}>
|
|
326
|
-
<Handle type="target" position={Position.Left} />
|
|
327
|
-
<span>{data.label}</span>
|
|
328
|
-
<input
|
|
329
|
-
type="color"
|
|
330
|
-
value={data.color}
|
|
331
|
-
onChange={(e) => data.onChange?.(e.target.value)}
|
|
332
|
-
style={{ pointerEvents: 'all' }} // Important! Nodes overlay has pointerEvents: none
|
|
333
|
-
/>
|
|
334
|
-
<Handle type="source" position={Position.Right} />
|
|
335
|
-
</div>
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// 2. Register it in nodeTypes (must be defined OUTSIDE the component, or memoized)
|
|
340
|
-
const nodeTypes = { colorPicker: ColorPickerNode };
|
|
341
|
-
|
|
342
|
-
// 3. Use it
|
|
343
|
-
const nodes = [
|
|
344
|
-
{
|
|
345
|
-
id: '1',
|
|
346
|
-
type: 'colorPicker', // matches the key in nodeTypes
|
|
347
|
-
position: { x: 0, y: 0 },
|
|
348
|
-
data: { label: 'Pick a color', color: '#ff6600' },
|
|
349
|
-
},
|
|
350
|
-
];
|
|
351
|
-
|
|
352
|
-
function App() {
|
|
353
|
-
return (
|
|
354
|
-
<InfiniteCanvas nodes={nodes} nodeTypes={nodeTypes} ... />
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**Important**: Nodes without a `type` (or with `type: 'default'`) are rendered by the canvas worker, NOT as React components. Only nodes with a `type` that matches a key in `nodeTypes` become React components.
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
|
-
## Custom Edges
|
|
364
|
-
|
|
365
|
-
Same pattern as custom nodes:
|
|
366
|
-
|
|
367
|
-
```jsx
|
|
368
|
-
import { EdgeLabelRenderer } from '@infinit-canvas/react';
|
|
369
|
-
|
|
370
|
-
function CustomEdge({ id, sourceX, sourceY, targetX, targetY, label, style }) {
|
|
371
|
-
// Draw your own SVG path
|
|
372
|
-
const path = `M ${sourceX} ${sourceY} L ${targetX} ${targetY}`;
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
<>
|
|
376
|
-
<path d={path} stroke="#ff0000" strokeWidth={2} fill="none" style={style} />
|
|
377
|
-
{label && (
|
|
378
|
-
<EdgeLabelRenderer>
|
|
379
|
-
<div style={{
|
|
380
|
-
position: 'absolute',
|
|
381
|
-
transform: `translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`,
|
|
382
|
-
pointerEvents: 'all',
|
|
383
|
-
}}>
|
|
384
|
-
{label}
|
|
385
|
-
</div>
|
|
386
|
-
</EdgeLabelRenderer>
|
|
387
|
-
)}
|
|
388
|
-
</>
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const edgeTypes = { custom: CustomEdge };
|
|
393
|
-
// Then use: { id: 'e1', source: '1', target: '2', type: 'custom' }
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
---
|
|
397
|
-
|
|
398
|
-
## State Management
|
|
399
|
-
|
|
400
|
-
### How data flows
|
|
401
|
-
|
|
402
|
-
```
|
|
403
|
-
Your component (owns nodes/edges state)
|
|
404
|
-
│
|
|
405
|
-
├── passes nodes, edges as props ──→ <InfiniteCanvas>
|
|
406
|
-
│
|
|
407
|
-
├── onNodesChange callback ◄──────── user drags/selects/deletes a node
|
|
408
|
-
│ └── you call applyNodeChanges() to update your state
|
|
409
|
-
│
|
|
410
|
-
├── onEdgesChange callback ◄──────── user selects/deletes an edge
|
|
411
|
-
│ └── you call applyEdgeChanges() to update your state
|
|
412
|
-
│
|
|
413
|
-
└── onConnect callback ◄──────────── user drags handle-to-handle
|
|
414
|
-
└── you call addEdge() to add the new edge
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
**You own the data.** The canvas tells you what changed via callbacks, and you decide whether to apply those changes. This is the "controlled component" pattern — same as a React `<input>` with `value` and `onChange`.
|
|
418
|
-
|
|
419
|
-
### useNodesState / useEdgesState
|
|
420
|
-
|
|
421
|
-
Convenience hooks that set up `useState` + the `onChange` handler for you:
|
|
422
|
-
|
|
423
|
-
```jsx
|
|
424
|
-
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
425
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
Equivalent to manually writing:
|
|
429
|
-
|
|
430
|
-
```jsx
|
|
431
|
-
const [nodes, setNodes] = useState(initialNodes);
|
|
432
|
-
const onNodesChange = useCallback(
|
|
433
|
-
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
|
434
|
-
[]
|
|
435
|
-
);
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
### InfiniteCanvasProvider
|
|
439
|
-
|
|
440
|
-
Wrap your tree in `<InfiniteCanvasProvider>` when you need hooks ABOVE the `<InfiniteCanvas>` component (e.g., for auto-layout, or accessing `useReactFlow()` from a sibling component):
|
|
441
|
-
|
|
442
|
-
```jsx
|
|
443
|
-
import { InfiniteCanvasProvider, InfiniteCanvas, useReactFlow } from '@infinit-canvas/react';
|
|
444
|
-
|
|
445
|
-
function Toolbar() {
|
|
446
|
-
// This works because InfiniteCanvasProvider is above us
|
|
447
|
-
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
|
448
|
-
return (
|
|
449
|
-
<div>
|
|
450
|
-
<button onClick={() => zoomIn()}>+</button>
|
|
451
|
-
<button onClick={() => zoomOut()}>-</button>
|
|
452
|
-
<button onClick={() => fitView()}>Fit</button>
|
|
453
|
-
</div>
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function App() {
|
|
458
|
-
return (
|
|
459
|
-
<InfiniteCanvasProvider>
|
|
460
|
-
<Toolbar />
|
|
461
|
-
<InfiniteCanvas nodes={nodes} edges={edges} ... />
|
|
462
|
-
</InfiniteCanvasProvider>
|
|
45
|
+
onConnect={onConnect}
|
|
46
|
+
height="500px"
|
|
47
|
+
>
|
|
48
|
+
<Controls />
|
|
49
|
+
<MiniMap />
|
|
50
|
+
<Background variant="dots" />
|
|
51
|
+
</InfiniteCanvas>
|
|
463
52
|
);
|
|
464
53
|
}
|
|
465
54
|
```
|
|
466
55
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
## Hooks
|
|
470
|
-
|
|
471
|
-
### useReactFlow()
|
|
472
|
-
|
|
473
|
-
Imperative API to control the canvas programmatically. Does NOT cause re-renders — uses refs internally.
|
|
474
|
-
|
|
475
|
-
```jsx
|
|
476
|
-
const {
|
|
477
|
-
// Read
|
|
478
|
-
getNodes, // () => Node[]
|
|
479
|
-
getEdges, // () => Edge[]
|
|
480
|
-
getNode, // (id) => Node | undefined
|
|
481
|
-
getEdge, // (id) => Edge | undefined
|
|
482
|
-
getViewport, // () => { x, y, zoom }
|
|
483
|
-
getZoom, // () => number
|
|
484
|
-
|
|
485
|
-
// Write
|
|
486
|
-
setNodes, // (nodes | updaterFn) => void
|
|
487
|
-
setEdges, // (edges | updaterFn) => void
|
|
488
|
-
addNodes, // (node | nodes) => void
|
|
489
|
-
addEdges, // (edge | edges) => void
|
|
490
|
-
deleteElements, // ({ nodes?, edges? }) => void
|
|
491
|
-
updateNodeData, // (nodeId, dataUpdate) => void
|
|
492
|
-
|
|
493
|
-
// Camera
|
|
494
|
-
setViewport, // ({ x?, y?, zoom? }, { duration? }) => void
|
|
495
|
-
zoomIn, // ({ duration? }) => void
|
|
496
|
-
zoomOut, // ({ duration? }) => void
|
|
497
|
-
zoomTo, // (zoom, { duration? }) => void
|
|
498
|
-
fitView, // ({ padding?, duration?, nodes? }) => void
|
|
499
|
-
fitBounds, // (bounds, { padding?, duration? }) => void
|
|
500
|
-
setCenter, // (x, y, { zoom?, duration? }) => void
|
|
501
|
-
|
|
502
|
-
// Coordinate conversion
|
|
503
|
-
screenToFlowPosition, // ({ x, y }) => { x, y } (screen pixels → world coords)
|
|
504
|
-
flowToScreenPosition, // ({ x, y }) => { x, y } (world coords → screen pixels)
|
|
505
|
-
|
|
506
|
-
// Serialize
|
|
507
|
-
toObject, // () => { nodes, edges, viewport }
|
|
508
|
-
} = useReactFlow();
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### useNodes() / useEdges()
|
|
56
|
+
## Why This?
|
|
512
57
|
|
|
513
|
-
|
|
58
|
+
| | React Flow | @infinit-canvas/react |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| Rendering | DOM (React divs + SVG) | OffscreenCanvas + Web Worker |
|
|
61
|
+
| Max nodes (60fps) | ~500 | 5,000+ |
|
|
62
|
+
| Main thread | Blocked during render | Always free |
|
|
63
|
+
| Custom nodes | React components | Hybrid: canvas + DOM overlay |
|
|
64
|
+
| API | React Flow API | Same API (drop-in compatible) |
|
|
514
65
|
|
|
515
|
-
|
|
516
|
-
const nodes = useNodes();
|
|
517
|
-
const edges = useEdges();
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
### useStore(selector, equalityFn?)
|
|
521
|
-
|
|
522
|
-
Subscribe to specific slices of the internal store. Use a **selector** to pick only what you need (avoids unnecessary re-renders):
|
|
523
|
-
|
|
524
|
-
```jsx
|
|
525
|
-
// Only re-renders when the node count changes
|
|
526
|
-
const nodeCount = useStore((state) => state.nodes.length);
|
|
527
|
-
|
|
528
|
-
// With custom equality function
|
|
529
|
-
const nodeIds = useStore(
|
|
530
|
-
(state) => state.nodes.map(n => n.id),
|
|
531
|
-
(a, b) => a.length === b.length && a.every((id, i) => id === b[i])
|
|
532
|
-
);
|
|
533
|
-
```
|
|
66
|
+
## Features
|
|
534
67
|
|
|
535
|
-
|
|
68
|
+
- **Full React Flow API** — nodes, edges, handles, connections, selection, drag, zoom, pan
|
|
69
|
+
- **Custom node & edge types** — use any React component
|
|
70
|
+
- **Built-in components** — Controls, MiniMap, Background, Panel, Handle, NodeResizer, NodeToolbar, EdgeLabelRenderer, ViewportPortal
|
|
71
|
+
- **All hooks** — useReactFlow, useNodes, useEdges, useViewport, useConnection, useNodesData, useOnViewportChange, useOnSelectionChange, useKeyPress, useStore, useStoreApi, + more
|
|
72
|
+
- **Sub-flows** — parentId, extent: 'parent'
|
|
73
|
+
- **Connection modes** — strict/loose, click-to-connect, validation
|
|
74
|
+
- **Selection** — click, Shift+click multi-select, Ctrl+A, selection box, selection drag events
|
|
75
|
+
- **Edge types** — bezier, straight, step, smoothstep, animated, labels
|
|
76
|
+
- **Edge routing** — automatic obstacle avoidance
|
|
77
|
+
- **Snap to grid** — configurable grid snapping
|
|
78
|
+
- **Undo/redo** — built-in history management
|
|
79
|
+
- **Delete validation** — onBeforeDelete async callback
|
|
80
|
+
- **Error handling** — onError callback
|
|
81
|
+
- **noDragClassName / noPanClassName** — prevent drag/pan on specific elements
|
|
82
|
+
- **Elevate on select** — bring nodes/edges to front on selection
|
|
83
|
+
- **Node virtualization** — only visible custom nodes are mounted in DOM
|
|
84
|
+
- **Frustum culling** — spatial grid index for O(visible) rendering
|
|
536
85
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
```jsx
|
|
540
|
-
const { x, y, zoom } = useViewport();
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### useOnViewportChange({ onChange, onStart, onEnd })
|
|
544
|
-
|
|
545
|
-
Listen to viewport changes without re-rendering your component:
|
|
546
|
-
|
|
547
|
-
```jsx
|
|
548
|
-
useOnViewportChange({
|
|
549
|
-
onChange: (viewport) => console.log('Moving:', viewport),
|
|
550
|
-
onStart: (viewport) => console.log('Pan/zoom started'),
|
|
551
|
-
onEnd: (viewport) => console.log('Pan/zoom ended'),
|
|
552
|
-
});
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### useOnSelectionChange({ onChange })
|
|
556
|
-
|
|
557
|
-
```jsx
|
|
558
|
-
useOnSelectionChange({
|
|
559
|
-
onChange: ({ nodes, edges }) => {
|
|
560
|
-
console.log('Selected nodes:', nodes);
|
|
561
|
-
console.log('Selected edges:', edges);
|
|
562
|
-
},
|
|
563
|
-
});
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
### useNodesData(nodeIds)
|
|
567
|
-
|
|
568
|
-
Get just the `data` for specific nodes:
|
|
569
|
-
|
|
570
|
-
```jsx
|
|
571
|
-
const data = useNodesData(['node-1', 'node-2']);
|
|
572
|
-
// [{ id: 'node-1', type: 'default', data: { label: '...' } }, ...]
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
### useNodeConnections(nodeId)
|
|
576
|
-
|
|
577
|
-
Get all edges connected to a node:
|
|
578
|
-
|
|
579
|
-
```jsx
|
|
580
|
-
const connections = useNodeConnections('node-1');
|
|
581
|
-
// Edge[]
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
### useHandleConnections({ nodeId, type, handleId? })
|
|
585
|
-
|
|
586
|
-
Get edges connected to a specific handle:
|
|
587
|
-
|
|
588
|
-
```jsx
|
|
589
|
-
const incoming = useHandleConnections({ nodeId: 'node-1', type: 'target' });
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### useKeyPress(key)
|
|
593
|
-
|
|
594
|
-
```jsx
|
|
595
|
-
const shiftPressed = useKeyPress('Shift');
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
### useUndoRedo({ maxHistorySize? })
|
|
599
|
-
|
|
600
|
-
```jsx
|
|
601
|
-
const { undo, redo, takeSnapshot, canUndo, canRedo } = useUndoRedo();
|
|
602
|
-
|
|
603
|
-
// Call takeSnapshot() before making changes
|
|
604
|
-
function onNodeDragStop() {
|
|
605
|
-
takeSnapshot();
|
|
606
|
-
}
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
### useNodeId()
|
|
610
|
-
|
|
611
|
-
Inside a custom node component, get the current node's ID:
|
|
612
|
-
|
|
613
|
-
```jsx
|
|
614
|
-
function MyNode({ data }) {
|
|
615
|
-
const nodeId = useNodeId();
|
|
616
|
-
return <div>I am {nodeId}</div>;
|
|
617
|
-
}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
---
|
|
621
|
-
|
|
622
|
-
## Viewport & Camera
|
|
623
|
-
|
|
624
|
-
### Initial camera position
|
|
625
|
-
|
|
626
|
-
```jsx
|
|
627
|
-
<InfiniteCanvas initialCamera={{ x: 100, y: 50, zoom: 1.5 }} />
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
### Fit all nodes on mount
|
|
631
|
-
|
|
632
|
-
```jsx
|
|
633
|
-
<InfiniteCanvas fitView />
|
|
634
|
-
<InfiniteCanvas fitView fitViewOptions={{ padding: 0.2, maxZoom: 2 }} />
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
### Zoom limits
|
|
638
|
-
|
|
639
|
-
```jsx
|
|
640
|
-
<InfiniteCanvas zoomMin={0.1} zoomMax={5} />
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
### Lock the camera to an area
|
|
644
|
-
|
|
645
|
-
```jsx
|
|
646
|
-
// User can't pan beyond these world coordinates
|
|
647
|
-
<InfiniteCanvas translateExtent={[[-1000, -1000], [1000, 1000]]} />
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
### Programmatic camera control
|
|
651
|
-
|
|
652
|
-
```jsx
|
|
653
|
-
const { setViewport, fitView, zoomTo, setCenter } = useReactFlow();
|
|
654
|
-
|
|
655
|
-
// Animated zoom to 2x over 500ms
|
|
656
|
-
zoomTo(2, { duration: 500 });
|
|
657
|
-
|
|
658
|
-
// Center on a specific world coordinate
|
|
659
|
-
setCenter(500, 300, { zoom: 1.5, duration: 300 });
|
|
660
|
-
|
|
661
|
-
// Fit all nodes with padding
|
|
662
|
-
fitView({ padding: 0.2, duration: 400 });
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
---
|
|
666
|
-
|
|
667
|
-
## Interactions
|
|
668
|
-
|
|
669
|
-
### Panning
|
|
670
|
-
|
|
671
|
-
| Method | Default |
|
|
672
|
-
|--------|---------|
|
|
673
|
-
| Click + drag on empty space | Always enabled |
|
|
674
|
-
| Scroll wheel | Zooms (set `panOnScroll` to pan instead) |
|
|
675
|
-
| Space + drag | Pan with spacebar held (`panActivationKeyCode`) |
|
|
86
|
+
## Props
|
|
676
87
|
|
|
677
88
|
```jsx
|
|
678
89
|
<InfiniteCanvas
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
90
|
+
// Data
|
|
91
|
+
nodes={nodes}
|
|
92
|
+
edges={edges}
|
|
93
|
+
onNodesChange={onNodesChange}
|
|
94
|
+
onEdgesChange={onEdgesChange}
|
|
95
|
+
onConnect={onConnect}
|
|
96
|
+
|
|
97
|
+
// Types
|
|
98
|
+
nodeTypes={nodeTypes}
|
|
99
|
+
edgeTypes={edgeTypes}
|
|
100
|
+
|
|
101
|
+
// Events
|
|
102
|
+
onNodeClick onNodeDoubleClick onNodeContextMenu
|
|
103
|
+
onNodeDragStart onNodeDrag onNodeDragStop
|
|
104
|
+
onEdgeClick onEdgeDoubleClick
|
|
105
|
+
onPaneClick onPaneContextMenu
|
|
106
|
+
onSelectionChange
|
|
107
|
+
onSelectionDragStart onSelectionDrag onSelectionDragStop
|
|
108
|
+
onConnectStart onConnectEnd
|
|
109
|
+
onMoveStart onMove onMoveEnd
|
|
110
|
+
onInit onDelete onBeforeDelete onError
|
|
111
|
+
|
|
112
|
+
// Behavior
|
|
113
|
+
nodesDraggable nodesConnectable elementsSelectable
|
|
114
|
+
connectionMode="loose" connectOnClick={false}
|
|
115
|
+
snapToGrid snapGrid={[15, 15]}
|
|
116
|
+
elevateNodesOnSelect elevateEdgesOnSelect
|
|
117
|
+
noDragClassName="nodrag" noPanClassName="nopan"
|
|
118
|
+
deleteKeyCode="Delete"
|
|
119
|
+
isValidConnection={fn}
|
|
120
|
+
|
|
121
|
+
// Viewport
|
|
122
|
+
fitView initialCamera={{ x: 0, y: 0, zoom: 1 }}
|
|
123
|
+
zoomMin={0.1} zoomMax={4}
|
|
124
|
+
panOnScroll zoomOnScroll zoomOnPinch zoomOnDoubleClick
|
|
125
|
+
translateExtent autoPanOnNodeDrag
|
|
126
|
+
|
|
127
|
+
// Appearance
|
|
128
|
+
dark gridSize={40} width="100%" height="500px"
|
|
683
129
|
/>
|
|
684
130
|
```
|
|
685
131
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
```jsx
|
|
689
|
-
<InfiniteCanvas
|
|
690
|
-
zoomOnScroll // default: true
|
|
691
|
-
zoomOnPinch // default: true (two-finger pinch on trackpad/mobile)
|
|
692
|
-
zoomOnDoubleClick // default: true
|
|
693
|
-
zoomMin={0.1}
|
|
694
|
-
zoomMax={5}
|
|
695
|
-
/>
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
### Node dragging
|
|
699
|
-
|
|
700
|
-
```jsx
|
|
701
|
-
<InfiniteCanvas
|
|
702
|
-
nodesDraggable // default: true — all nodes can be dragged
|
|
703
|
-
snapToGrid // snap to grid while dragging
|
|
704
|
-
snapGrid={[25, 25]} // grid spacing [x, y] in pixels
|
|
705
|
-
nodeExtent={[[-500, -500], [500, 500]]} // limit where nodes can be dragged
|
|
706
|
-
/>
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
### Connecting nodes
|
|
710
|
-
|
|
711
|
-
```jsx
|
|
712
|
-
<InfiniteCanvas
|
|
713
|
-
nodesConnectable // default: true
|
|
714
|
-
connectionMode="loose" // 'strict' = only source→target, 'loose' = any handle
|
|
715
|
-
connectionRadius={20} // snap distance in pixels
|
|
716
|
-
connectOnClick // click handles instead of dragging
|
|
717
|
-
isValidConnection={(conn) => conn.source !== conn.target} // validation
|
|
718
|
-
defaultEdgeOptions={{ type: 'smoothstep', animated: true }} // defaults for new edges
|
|
719
|
-
onConnect={(connection) => ...}
|
|
720
|
-
/>
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
### Selection
|
|
724
|
-
|
|
725
|
-
```jsx
|
|
726
|
-
<InfiniteCanvas
|
|
727
|
-
elementsSelectable // default: true
|
|
728
|
-
multiSelectionKeyCode="Shift" // hold Shift to select multiple
|
|
729
|
-
selectionOnDrag // drag on empty space = selection box
|
|
730
|
-
selectionMode="partial" // 'partial' = touching box, 'full' = fully inside
|
|
731
|
-
onSelectionChange={({ nodes, edges }) => ...}
|
|
732
|
-
/>
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
### Delete
|
|
736
|
-
|
|
737
|
-
```jsx
|
|
738
|
-
<InfiniteCanvas
|
|
739
|
-
deleteKeyCode={['Delete', 'Backspace']} // keys that delete selected elements
|
|
740
|
-
onDelete={({ nodes, edges }) => console.log('Deleted:', nodes, edges)}
|
|
741
|
-
onBeforeDelete={async ({ nodes, edges }) => {
|
|
742
|
-
return window.confirm('Are you sure?'); // return false to cancel
|
|
743
|
-
}}
|
|
744
|
-
/>
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
### Drag & Drop (external)
|
|
748
|
-
|
|
749
|
-
```jsx
|
|
750
|
-
<InfiniteCanvas
|
|
751
|
-
onDragOver={(e) => e.preventDefault()}
|
|
752
|
-
onDrop={(e) => {
|
|
753
|
-
const type = e.dataTransfer.getData('application/reactflow');
|
|
754
|
-
const position = screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
|
755
|
-
addNodes({ id: Date.now().toString(), type, position, data: { label: type } });
|
|
756
|
-
}}
|
|
757
|
-
/>
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
---
|
|
761
|
-
|
|
762
|
-
## Styling & Appearance
|
|
763
|
-
|
|
764
|
-
### CSS
|
|
765
|
-
|
|
766
|
-
```jsx
|
|
767
|
-
import '@infinit-canvas/react/styles.css'; // Required base styles
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
### Dark mode
|
|
771
|
-
|
|
772
|
-
```jsx
|
|
773
|
-
<InfiniteCanvas dark /> // force dark
|
|
774
|
-
<InfiniteCanvas dark={false} /> // force light
|
|
775
|
-
// omit dark prop → auto-detect from system preference
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Background grid
|
|
779
|
-
|
|
780
|
-
```jsx
|
|
781
|
-
import { Background } from '@infinit-canvas/react';
|
|
782
|
-
|
|
783
|
-
<InfiniteCanvas>
|
|
784
|
-
<Background variant="dots" gap={20} size={1} color="#aaa" />
|
|
785
|
-
</InfiniteCanvas>
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
Variants: `'lines'` (default), `'dots'`, `'cross'`
|
|
789
|
-
|
|
790
|
-
### Container sizing
|
|
791
|
-
|
|
792
|
-
```jsx
|
|
793
|
-
<InfiniteCanvas width="100%" height="100vh" />
|
|
794
|
-
<InfiniteCanvas width={800} height={600} />
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
### HUD overlay
|
|
798
|
-
|
|
799
|
-
```jsx
|
|
800
|
-
<InfiniteCanvas
|
|
801
|
-
showHud // shows world coords + zoom (default: true)
|
|
802
|
-
showHint // shows "Drag to pan - Scroll to zoom" hint (default: true)
|
|
803
|
-
hintText="Custom hint text"
|
|
804
|
-
/>
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
---
|
|
808
|
-
|
|
809
|
-
## Components
|
|
810
|
-
|
|
811
|
-
### Controls
|
|
812
|
-
|
|
813
|
-
Zoom and fit-view buttons:
|
|
814
|
-
|
|
815
|
-
```jsx
|
|
816
|
-
import { Controls } from '@infinit-canvas/react';
|
|
817
|
-
|
|
818
|
-
<InfiniteCanvas>
|
|
819
|
-
<Controls position="bottom-right" />
|
|
820
|
-
</InfiniteCanvas>
|
|
821
|
-
```
|
|
822
|
-
|
|
823
|
-
### MiniMap
|
|
824
|
-
|
|
825
|
-
Bird's-eye overview:
|
|
826
|
-
|
|
827
|
-
```jsx
|
|
828
|
-
import { MiniMap } from '@infinit-canvas/react';
|
|
829
|
-
|
|
830
|
-
<InfiniteCanvas>
|
|
831
|
-
<MiniMap
|
|
832
|
-
width={200}
|
|
833
|
-
height={150}
|
|
834
|
-
nodeColor={(node) => node.selected ? '#ff0' : '#eee'}
|
|
835
|
-
/>
|
|
836
|
-
</InfiniteCanvas>
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
### Panel
|
|
840
|
-
|
|
841
|
-
Position any content relative to the canvas:
|
|
842
|
-
|
|
843
|
-
```jsx
|
|
844
|
-
import { Panel } from '@infinit-canvas/react';
|
|
845
|
-
|
|
846
|
-
<InfiniteCanvas>
|
|
847
|
-
<Panel position="top-left">
|
|
848
|
-
<h3>My Flow</h3>
|
|
849
|
-
</Panel>
|
|
850
|
-
</InfiniteCanvas>
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
Positions: `'top-left'` | `'top-right'` | `'top-center'` | `'bottom-left'` | `'bottom-right'` | `'bottom-center'`
|
|
854
|
-
|
|
855
|
-
### NodeResizer
|
|
856
|
-
|
|
857
|
-
Add resize handles to custom nodes:
|
|
858
|
-
|
|
859
|
-
```jsx
|
|
860
|
-
import { NodeResizer } from '@infinit-canvas/react';
|
|
861
|
-
|
|
862
|
-
function ResizableNode({ data, selected }) {
|
|
863
|
-
return (
|
|
864
|
-
<>
|
|
865
|
-
<NodeResizer isVisible={selected} minWidth={100} minHeight={50} />
|
|
866
|
-
<div>{data.label}</div>
|
|
867
|
-
</>
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
### NodeToolbar
|
|
873
|
-
|
|
874
|
-
Floating toolbar that follows a node:
|
|
875
|
-
|
|
876
|
-
```jsx
|
|
877
|
-
import { NodeToolbar } from '@infinit-canvas/react';
|
|
878
|
-
|
|
879
|
-
function MyNode({ data, id }) {
|
|
880
|
-
return (
|
|
881
|
-
<div>
|
|
882
|
-
<NodeToolbar position={Position.Top}>
|
|
883
|
-
<button>Edit</button>
|
|
884
|
-
<button>Delete</button>
|
|
885
|
-
</NodeToolbar>
|
|
886
|
-
{data.label}
|
|
887
|
-
</div>
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
```
|
|
891
|
-
|
|
892
|
-
### EdgeLabelRenderer
|
|
893
|
-
|
|
894
|
-
Portal for edge labels (renders in the edge label overlay layer):
|
|
895
|
-
|
|
896
|
-
```jsx
|
|
897
|
-
import { EdgeLabelRenderer } from '@infinit-canvas/react';
|
|
898
|
-
|
|
899
|
-
function MyEdge({ sourceX, sourceY, targetX, targetY }) {
|
|
900
|
-
const midX = (sourceX + targetX) / 2;
|
|
901
|
-
const midY = (sourceY + targetY) / 2;
|
|
902
|
-
|
|
903
|
-
return (
|
|
904
|
-
<>
|
|
905
|
-
<path d={`M ${sourceX} ${sourceY} L ${targetX} ${targetY}`} stroke="#333" />
|
|
906
|
-
<EdgeLabelRenderer>
|
|
907
|
-
<div style={{
|
|
908
|
-
position: 'absolute',
|
|
909
|
-
transform: `translate(-50%, -50%) translate(${midX}px, ${midY}px)`,
|
|
910
|
-
pointerEvents: 'all',
|
|
911
|
-
}}>
|
|
912
|
-
Label
|
|
913
|
-
</div>
|
|
914
|
-
</EdgeLabelRenderer>
|
|
915
|
-
</>
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
### ViewportPortal
|
|
921
|
-
|
|
922
|
-
Render any React content that moves with the viewport:
|
|
923
|
-
|
|
924
|
-
```jsx
|
|
925
|
-
import { ViewportPortal } from '@infinit-canvas/react';
|
|
926
|
-
|
|
927
|
-
<InfiniteCanvas>
|
|
928
|
-
<ViewportPortal>
|
|
929
|
-
<div style={{ position: 'absolute', left: 100, top: 100 }}>
|
|
930
|
-
I'm at world position (100, 100)
|
|
931
|
-
</div>
|
|
932
|
-
</ViewportPortal>
|
|
933
|
-
</InfiniteCanvas>
|
|
934
|
-
```
|
|
132
|
+
## Hooks
|
|
935
133
|
|
|
936
|
-
|
|
134
|
+
| Hook | Description |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `useNodesState` | useState + applyNodeChanges |
|
|
137
|
+
| `useEdgesState` | useState + applyEdgeChanges |
|
|
138
|
+
| `useReactFlow` | Imperative API (fitView, zoomIn, getNodes, setNodes, etc.) |
|
|
139
|
+
| `useNodes` / `useEdges` | Read current nodes/edges |
|
|
140
|
+
| `useViewport` | Read { x, y, zoom } |
|
|
141
|
+
| `useConnection` | Connection state while dragging |
|
|
142
|
+
| `useOnViewportChange` | Subscribe to viewport changes |
|
|
143
|
+
| `useOnSelectionChange` | Subscribe to selection changes |
|
|
144
|
+
| `useKeyPress` | Track key state |
|
|
145
|
+
| `useStore` / `useStoreApi` | Direct store access |
|
|
146
|
+
| `useUndoRedo` | Undo/redo history |
|
|
937
147
|
|
|
938
148
|
## Utilities
|
|
939
149
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
Apply an array of changes to your nodes array. Used in `onNodesChange`:
|
|
943
|
-
|
|
944
|
-
```jsx
|
|
945
|
-
const onNodesChange = (changes) => setNodes((nds) => applyNodeChanges(changes, nds));
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
### applyEdgeChanges(changes, edges) → edges
|
|
949
|
-
|
|
950
|
-
Same for edges:
|
|
951
|
-
|
|
952
|
-
```jsx
|
|
953
|
-
const onEdgesChange = (changes) => setEdges((eds) => applyEdgeChanges(changes, eds));
|
|
954
|
-
```
|
|
955
|
-
|
|
956
|
-
### addEdge(connection, edges) → edges
|
|
957
|
-
|
|
958
|
-
Add a new edge from a connection event:
|
|
959
|
-
|
|
960
|
-
```jsx
|
|
961
|
-
const onConnect = (connection) => setEdges((eds) => addEdge(connection, eds));
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
### getConnectedEdges(nodes, edges) → edges
|
|
965
|
-
|
|
966
|
-
Find all edges connected to a set of nodes:
|
|
967
|
-
|
|
968
|
-
```jsx
|
|
969
|
-
const connected = getConnectedEdges(selectedNodes, allEdges);
|
|
970
|
-
```
|
|
971
|
-
|
|
972
|
-
### getIncomers(node, nodes, edges) / getOutgoers(node, nodes, edges)
|
|
973
|
-
|
|
974
|
-
Find upstream/downstream nodes:
|
|
975
|
-
|
|
976
|
-
```jsx
|
|
977
|
-
const parents = getIncomers(myNode, allNodes, allEdges);
|
|
978
|
-
const children = getOutgoers(myNode, allNodes, allEdges);
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
### getNodesBounds(nodes) → { x, y, width, height }
|
|
982
|
-
|
|
983
|
-
Compute the bounding box of a set of nodes:
|
|
984
|
-
|
|
985
|
-
```jsx
|
|
986
|
-
const bounds = getNodesBounds(selectedNodes);
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
---
|
|
990
|
-
|
|
991
|
-
## Performance
|
|
992
|
-
|
|
993
|
-
This library is designed for large graphs (5000+ nodes). Here's how:
|
|
994
|
-
|
|
995
|
-
| Technique | What it does |
|
|
996
|
-
|-----------|-------------|
|
|
997
|
-
| **OffscreenCanvas Worker** | All drawing happens on a separate thread. Your React UI and event handlers never wait for rendering. |
|
|
998
|
-
| **Spatial grid culling** | Only visible nodes/edges are drawn. 5000 nodes on canvas but only 50 in view = only 50 drawn. |
|
|
999
|
-
| **Batched rendering** | All nodes drawn in 2-3 draw calls total (not one per node). Same for edges. |
|
|
1000
|
-
| **Camera as ref** | Panning/zooming never triggers React re-renders. Camera position is a mutable ref, not React state. |
|
|
1001
|
-
| **DOM transform sync** | Custom React nodes move via direct `element.style.transform` mutations in a rAF loop — no React reconciliation during pan/zoom. |
|
|
1002
|
-
| **Hit test skipping** | Mouse hover detection (O(n) scan) is skipped entirely during active pan, drag, connect, or selection. |
|
|
1003
|
-
| **Node virtualization** | Only visible custom React nodes are mounted in the DOM. Off-screen nodes are unmounted. |
|
|
1004
|
-
| **Grid tile caching** | Background grid is rendered once to a small canvas tile, then tiled via `createPattern('repeat')`. |
|
|
1005
|
-
| **Level of Detail** | Text labels, shadows, and arrows are skipped at low zoom levels where they'd be invisible anyway. |
|
|
1006
|
-
| **Edge routing** | Runs asynchronously after rendering, using spatial grid lookups for nearby obstacle nodes. |
|
|
1007
|
-
| **Incremental drag** | During drag, only changed node positions are sent to the worker — not the entire node array. |
|
|
1008
|
-
|
|
1009
|
-
---
|
|
1010
|
-
|
|
1011
|
-
## Full Props Reference
|
|
1012
|
-
|
|
1013
|
-
### Data
|
|
1014
|
-
|
|
1015
|
-
| Prop | Type | Default | Description |
|
|
1016
|
-
|------|------|---------|-------------|
|
|
1017
|
-
| `nodes` | `Node[]` | `[]` | Array of nodes to display |
|
|
1018
|
-
| `edges` | `Edge[]` | `[]` | Array of edges connecting nodes |
|
|
1019
|
-
| `cards` | `Card[]` | `[]` | Legacy card-based data (simple rectangles) |
|
|
1020
|
-
| `nodeTypes` | `Record<string, Component>` | built-ins | Map of type name → React component for custom nodes |
|
|
1021
|
-
| `edgeTypes` | `Record<string, Component>` | built-ins | Map of type name → React component for custom edges |
|
|
1022
|
-
|
|
1023
|
-
### Appearance
|
|
1024
|
-
|
|
1025
|
-
| Prop | Type | Default | Description |
|
|
1026
|
-
|------|------|---------|-------------|
|
|
1027
|
-
| `dark` | `boolean` | system | Force dark or light theme |
|
|
1028
|
-
| `gridSize` | `number` | `40` | Background grid spacing in world pixels |
|
|
1029
|
-
| `width` | `string \| number` | `'100%'` | Container width |
|
|
1030
|
-
| `height` | `string \| number` | `'420px'` | Container height |
|
|
1031
|
-
| `className` | `string` | `''` | CSS class on wrapper div |
|
|
1032
|
-
| `style` | `CSSProperties` | `{}` | Inline styles on wrapper div |
|
|
1033
|
-
|
|
1034
|
-
### Camera
|
|
1035
|
-
|
|
1036
|
-
| Prop | Type | Default | Description |
|
|
1037
|
-
|------|------|---------|-------------|
|
|
1038
|
-
| `initialCamera` | `{ x, y, zoom }` | `{ x:0, y:0, zoom:1 }` | Starting camera position |
|
|
1039
|
-
| `fitView` | `boolean` | `false` | Auto-zoom to fit all nodes on mount |
|
|
1040
|
-
| `fitViewOptions` | `FitViewOptions` | — | Options for fitView (padding, maxZoom, etc.) |
|
|
1041
|
-
| `zoomMin` | `number` | `0.1` | Minimum zoom level |
|
|
1042
|
-
| `zoomMax` | `number` | `4` | Maximum zoom level |
|
|
1043
|
-
| `translateExtent` | `[[x,y],[x,y]]` | — | Camera movement bounds |
|
|
150
|
+
`applyNodeChanges` · `applyEdgeChanges` · `addEdge` · `isNode` · `isEdge` · `getConnectedEdges` · `getIncomers` · `getOutgoers` · `getNodesBounds` · `getBezierPath` · `getSmoothStepPath` · `getStraightPath` · `getSimpleBezierPath` · `snapPosition` · `reconnectEdge`
|
|
1044
151
|
|
|
1045
|
-
|
|
152
|
+
## Sponsor
|
|
1046
153
|
|
|
1047
|
-
|
|
1048
|
-
|------|------|---------|-------------|
|
|
1049
|
-
| `nodesDraggable` | `boolean` | `true` | Can nodes be dragged? |
|
|
1050
|
-
| `nodesConnectable` | `boolean` | `true` | Can edges be created by dragging handles? |
|
|
1051
|
-
| `elementsSelectable` | `boolean` | `true` | Can nodes/edges be clicked to select? |
|
|
1052
|
-
| `snapToGrid` | `boolean` | `false` | Snap node positions to grid during drag |
|
|
1053
|
-
| `snapGrid` | `[number, number]` | `[15, 15]` | Grid spacing for snap |
|
|
1054
|
-
| `connectionMode` | `'strict' \| 'loose'` | `'loose'` | Strict: source→target only. Loose: any handle. |
|
|
1055
|
-
| `connectionRadius` | `number` | `20` | Snap-to-handle distance in pixels |
|
|
1056
|
-
| `connectOnClick` | `boolean` | `false` | Click handles to connect (instead of drag) |
|
|
1057
|
-
| `selectionOnDrag` | `boolean` | `false` | Drag on empty space creates selection box |
|
|
1058
|
-
| `selectionMode` | `'partial' \| 'full'` | `'partial'` | How selection box selects nodes |
|
|
1059
|
-
| `multiSelectionKeyCode` | `string` | `'Shift'` | Key to hold for multi-select |
|
|
1060
|
-
| `deleteKeyCode` | `string \| string[]` | `['Delete', 'Backspace']` | Keys that delete selected elements |
|
|
1061
|
-
| `panActivationKeyCode` | `string` | `' '` (space) | Key that activates pan mode |
|
|
1062
|
-
| `panOnScroll` | `boolean` | `false` | Scroll wheel pans instead of zooming |
|
|
1063
|
-
| `zoomOnScroll` | `boolean` | `true` | Scroll wheel zooms |
|
|
1064
|
-
| `zoomOnPinch` | `boolean` | `true` | Pinch gesture zooms |
|
|
1065
|
-
| `zoomOnDoubleClick` | `boolean` | `true` | Double-click zooms in |
|
|
1066
|
-
| `edgeRouting` | `boolean` | `true` | Smart edge paths that avoid nodes |
|
|
1067
|
-
| `edgesReconnectable` | `boolean` | `false` | Can existing edges be re-routed to different handles |
|
|
1068
|
-
| `elevateNodesOnSelect` | `boolean` | `false` | Bring selected nodes to front |
|
|
1069
|
-
| `autoPanOnNodeDrag` | `boolean` | `true` | Auto-scroll when dragging near edge |
|
|
1070
|
-
| `autoPanOnConnect` | `boolean` | `true` | Auto-scroll when connecting near edge |
|
|
1071
|
-
| `nodeExtent` | `[[x,y],[x,y]]` | — | Limit where nodes can be dragged |
|
|
154
|
+
If this package saves you time, consider supporting its development:
|
|
1072
155
|
|
|
1073
|
-
|
|
156
|
+
[](https://github.com/sponsors/awaisshah228)
|
|
1074
157
|
|
|
1075
|
-
|
|
1076
|
-
|------|-----------|-------------|
|
|
1077
|
-
| `onNodesChange` | `(changes: NodeChange[]) => void` | Node was dragged, selected, added, or removed |
|
|
1078
|
-
| `onEdgesChange` | `(changes: EdgeChange[]) => void` | Edge was selected, added, or removed |
|
|
1079
|
-
| `onConnect` | `(connection: Connection) => void` | User connected two handles |
|
|
1080
|
-
| `onNodeClick` | `(event, node) => void` | Node was clicked |
|
|
1081
|
-
| `onNodeDoubleClick` | `(event, node) => void` | Node was double-clicked |
|
|
1082
|
-
| `onNodeDragStart` | `(event, node) => void` | Started dragging a node |
|
|
1083
|
-
| `onNodeDrag` | `(event, node) => void` | Dragging a node (fires every frame) |
|
|
1084
|
-
| `onNodeDragStop` | `(event, node) => void` | Finished dragging a node |
|
|
1085
|
-
| `onEdgeClick` | `(event, edge) => void` | Edge was clicked |
|
|
1086
|
-
| `onPaneClick` | `(event) => void` | Clicked empty canvas space |
|
|
1087
|
-
| `onSelectionChange` | `({ nodes, edges }) => void` | Selection changed |
|
|
1088
|
-
| `onInit` | `(instance) => void` | Canvas initialized (receives ReactFlowInstance) |
|
|
1089
|
-
| `onMove` | `(event, viewport) => void` | Camera moved (pan/zoom) |
|
|
1090
|
-
| `onDelete` | `({ nodes, edges }) => void` | Elements were deleted |
|
|
1091
|
-
| `onBeforeDelete` | `({ nodes, edges }) => Promise<boolean>` | Return false to prevent deletion |
|
|
1092
|
-
| `onConnect` | `(connection) => void` | New connection created |
|
|
1093
|
-
| `isValidConnection` | `(connection) => boolean` | Validate before allowing a connection |
|
|
158
|
+
**USDC (Solana):** `59FhVxK3uxABiJ9VzXtCoyCxqq4nhoZDBtUV3gEkiexo`
|
|
1094
159
|
|
|
1095
|
-
|
|
160
|
+
<img src="https://raw.githubusercontent.com/awaisshah228/react-flow-avoid-nodes-routing/turbo-package/assets/solana-donate-qr.png" width="200" alt="Solana USDC QR Code" />
|
|
1096
161
|
|
|
1097
162
|
## License
|
|
1098
163
|
|
|
1099
|
-
MIT
|
|
164
|
+
[MIT](./LICENSE)
|