Migrating to v5
v5 replaces per-edge, main-thread-only routing with a shared SmartEdgeProvider that batches every registered edge and, by default, runs that batch on a background Web Worker. This is the overhaul behind #69 (large graphs freezing the tab). It changes how you wire up smart edges and removes the 4.13 batch-routing API that SmartEdgeProvider now supersedes.
SmartEdgeProvider is required
Every smart edge (SmartBezierEdge, createSmartEdge output, SmartEdge, SmartFloatingEdge, SmartEditableEdge, SmartCheckpointEdge) now routes through the nearest SmartEdgeProvider. Without one, an edge warns once in development and renders its native (non-routed) fallback edge instead: the graph still renders, it just never routes.
Before (v4):
import { ReactFlow } from "@xyflow/react";
import { SmartBezierEdge } from "@tisoap/react-flow-smart-edge";
const edgeTypes = { smart: SmartBezierEdge };
function Flow({ nodes, edges }) {
return <ReactFlow nodes={nodes} edges={edges} edgeTypes={edgeTypes} />;
}
After (v5):
import { ReactFlow } from "@xyflow/react";
import {
SmartEdgeProvider,
SmartBezierEdge,
} from "@tisoap/react-flow-smart-edge";
const edgeTypes = { smart: SmartBezierEdge };
function Flow({ nodes, edges }) {
return (
<SmartEdgeProvider nodes={nodes}>
<ReactFlow nodes={nodes} edges={edges} edgeTypes={edgeTypes} />
</SmartEdgeProvider>
);
}
Wrap once, near the top of your flow. SmartEdgeProvider does not need to be inside <ReactFlow> or a ReactFlowProvider; every edge registers its own geometry with it directly.
Nodes must be controlled
SmartEdgeProvider reads its nodes prop directly, so it needs the live array on every render. If you currently hand React Flow uncontrolled defaultNodes, lift that state:
// Before: uncontrolled, no provider needed
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
edgeTypes={edgeTypes}
/>;
// After: nodes controlled, provider reads the live array
const [nodes, , onNodesChange] = useNodesState(initialNodes);
<SmartEdgeProvider nodes={nodes}>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
defaultEdges={initialEdges}
edgeTypes={edgeTypes}
/>
</SmartEdgeProvider>;
Edges do not need to be controlled for routing purposes; defaultEdges (or your own edge state) works fine alongside a controlled nodes array.
Flags that restore v4 behavior
v5 ships new defaults aimed at large graphs. Set these on SmartEdgeProvider's options to get v4's always-route, always-live behavior back:
| Option | v5 default | Set to restore v4 behavior |
|---|---|---|
routeOnlyWhenBlocked | true | false: always pathfind, even when the direct line is already clear |
routeWhileDragging | false | true: re-route on every drag frame instead of showing a fallback until drop |
<SmartEdgeProvider
nodes={nodes}
options={{
routeOnlyWhenBlocked: false,
routeWhileDragging: true,
}}
>
<ReactFlow nodes={nodes} edges={edges} edgeTypes={edgeTypes} />
</SmartEdgeProvider>
Removed: the 4.13 batch-routing API
SmartEdgeBatchRoutingProvider and useSmartEdgeRoute (added in 4.13 as an opt-in worker path) are removed. SmartEdgeProvider is their non-opt-in, always-on replacement: every smart edge now gets the batching and worker offload that used to be a separate provider.
| 4.13 | v5 | Notes |
|---|---|---|
SmartEdgeBatchRoutingProvider | SmartEdgeProvider | Same "wrap your flow once" role. Now required by every smart edge, not just a worker-typed custom edge. |
useSmartEdgeRoute(props) | useSmartEdgePath(input) | input is an explicit object (id, source, target, endpoints, sourcePosition/targetPosition, optional preset/options/waypoints), not the raw EdgeProps. The result is { route, isDragging, hasProvider }; route is null while pending, or { kind: "clear" | "routed", wasRouted, ... }. |
edge.data.smartEdge (per-edge override) | createSmartEdge(preset, options) per edge type, or explicit preset/options passed into useSmartEdgePath | Routing options are no longer read from a reserved edge.data key. Bake them into the edge type at module scope, or read your own data shape in a custom edge and forward the values you want as useSmartEdgePath's preset/options. |
routeSmartEdgesBatch({ nodes, edges }) | routeSmartEdgeBatch(nodes, edges) | Still the pure batching function the worker (and main-thread fallback) run; now takes positional arguments. |
BatchEdgeInput, BatchRoutingInput, BatchRoutingResults, SmartEdgeBatchOptions, EdgeRouteInput types | SmartEdgeBatchItem, SmartEdgeBatchItemOptions, UseSmartEdgePathInput, UseSmartEdgePathResult | Renamed and restructured alongside the API above. |
Custom generatePath: PathFindingFunction now takes a FlatGrid
The routing engine moved from an object-based grid (one JS object per cell) to a typed-array FlatGrid for performance (see bench/RESULTS.md). If you supply a custom generatePath, its signature is unchanged in shape but its grid argument's contract changed:
// v4
type PathFindingFunction = (
grid: Grid,
start: XYPosition,
end: XYPosition,
) => number[][];
// Grid: an object with isWalkableAt(x, y), setWalkableAt(x, y, walkable), clone(), etc.
// v5
type PathFindingFunction = (
grid: FlatGrid,
start: XYPosition,
end: XYPosition,
) => number[][];
// FlatGrid: { width, height, blocked: Uint8Array }, index = y * width + x
Use the exported helpers instead of the old method calls: createFlatGrid(width, height), cloneFlatGrid(grid), isInside(grid, x, y), isWalkable(grid, x, y), setBlocked(grid, x, y, blocked), blockCellRange(grid, columnStart, rowStart, columnEnd, rowEnd). The built-in pathfindingAStarDiagonal, pathfindingAStarNoDiagonal, and pathfindingJumpPointNoDiagonal already target the new contract; only a fully custom generatePath needs updating.
wasRouted on results
GetSmartEdgeReturn (from getSmartEdge) gained a wasRouted: true field. It is always true for a synchronous getSmartEdge call; the field exists so it lines up with the provider's SmartEdgeRouteResult, whose "clear" variant carries wasRouted: false, so consumers can style an actually-routed path differently from a skipped/pending one without inspecting kind directly.
Custom drawEdge / generatePath only work with getSmartEdge
createSmartEdge(preset, { drawEdge, generatePath }) still type-checks, but
those function options cannot cross the routing worker. Under
SmartEdgeProvider, the worker resolves drawing and pathfinding from the
edge's preset. Per-edge serializable options (gridRatio, nodePadding,
avoidAreas, borderRadius) still apply.
To use a custom drawEdge or generatePath, call getSmartEdge yourself
(main thread) and render BaseEdge with the returned svgPathString. See
Custom smart edges.
What stays the same
Presets, createSmartEdge serializable options, smartEdgePresets,
getSmartEdge's core params, floating edges, checkpoints, hops, and
avoidAreas keep their v4 shape. Editable edges still persist waypoints on
edge.data.points; dragging a waypoint re-routes after debounceMs and
does not switch to the pending/drag native placeholder (that wrapper is only
for node drag and first-paint pending, not waypoint drag).
If your app only used preset edge components without the 4.13 batch API, add
SmartEdgeProvider, control nodes, and you are done — unless you passed
custom drawEdge / generatePath into createSmartEdge, which now need
getSmartEdge.