Skip to main content

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:

Optionv5 defaultSet to restore v4 behavior
routeOnlyWhenBlockedtruefalse: always pathfind, even when the direct line is already clear
routeWhileDraggingfalsetrue: 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.13v5Notes
SmartEdgeBatchRoutingProviderSmartEdgeProviderSame "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 useSmartEdgePathRouting 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 typesSmartEdgeBatchItem, SmartEdgeBatchItemOptions, UseSmartEdgePathInput, UseSmartEdgePathResultRenamed 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.