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.