Reactor.Community.ReactorFlow
0.4.0-preview.1
See the version list below for details.
dotnet add package Reactor.Community.ReactorFlow --version 0.4.0-preview.1
NuGet\Install-Package Reactor.Community.ReactorFlow -Version 0.4.0-preview.1
<PackageReference Include="Reactor.Community.ReactorFlow" Version="0.4.0-preview.1" />
<PackageVersion Include="Reactor.Community.ReactorFlow" Version="0.4.0-preview.1" />
<PackageReference Include="Reactor.Community.ReactorFlow" />
paket add Reactor.Community.ReactorFlow --version 0.4.0-preview.1
#r "nuget: Reactor.Community.ReactorFlow, 0.4.0-preview.1"
#:package Reactor.Community.ReactorFlow@0.4.0-preview.1
#addin nuget:?package=Reactor.Community.ReactorFlow&version=0.4.0-preview.1&prerelease
#tool nuget:?package=Reactor.Community.ReactorFlow&version=0.4.0-preview.1&prerelease
Reactor.Community.ReactorFlow
A declarative, node-based graph / canvas library for Microsoft.UI.Reactor, inspired by React Flow. If you build node editors, flow diagrams, pipelines, mind maps or whiteboards on the web with React Flow, ReactorFlow gives you the same mental model — nodes, edges, handles, a pan/zoom viewport, and opt-in chrome — natively in WinUI.
It is standalone: it draws its own edge paths (bezier / step / smooth-step / straight) and depends only on Microsoft.UI.Reactor and Microsoft.WindowsAppSDK. Nothing is tied to any particular application.
dotnet add package Reactor.Community.ReactorFlow
Hello, flow
using Reactor.Community.ReactorFlow;
using static Microsoft.UI.Reactor.Factories;
public sealed class MyBoard : Component
{
public override Element Render()
{
var (nodes, setNodes) = UseState<IReadOnlyList<ReactorFlowNode>>(new[]
{
new ReactorFlowNode("a", 80, 120, 160, 64, Data: "Source",
Ports: new[] { new ReactorFlowPort("out", ReactorFlowPosition.Right) }),
new ReactorFlowNode("b", 360, 120, 160, 64, Data: "Sink",
Ports: new[] { new ReactorFlowPort("in", ReactorFlowPosition.Left) }),
});
var edges = new[] { new ReactorFlowEdge("a-b", "a", "b", "out", "in", Label: "flow") };
return Component<ReactorFlow, ReactorFlowProps>(new ReactorFlowProps(
Nodes: nodes,
Edges: edges,
OnNodesChange: changes => setNodes(ReactorFlow.ApplyNodeChanges(changes, nodes)),
Background: ReactorFlow.Background(),
Overlays: new[]
{
ReactorFlow.MiniMap(),
ReactorFlow.Controls(),
}));
}
}
Concepts
ReactorFlow keeps React Flow's vocabulary so the migration is mostly mechanical:
| React Flow | ReactorFlow |
|---|---|
<ReactFlow nodes edges /> |
Component<ReactorFlow, ReactorFlowProps>(...) |
Node |
ReactorFlowNode (data-only record: id, position, size, type, data, ports) |
Edge |
ReactorFlowEdge (source/target + optional ports, kind, label, animated) |
Handle (Position.Left…) |
ReactorFlowPort on a node, or ReactorFlow.Handle(...) in custom content |
nodeTypes |
IReadOnlyDictionary<string, Func<ReactorFlowNodeContext, Element>> |
| custom handle content | PortTypes (Func<ReactorFlowPortContext, Element> per ReactorFlowPort.Type) |
onConnect(connection) |
OnConnect(ReactorFlowConnection) |
isValidConnection(connection) |
IsValidConnection(ReactorFlowConnection) => bool |
onNodeClick / onEdgeClick |
OnNodeClick / OnEdgeClick |
onNodeDoubleClick / onNodeContextMenu |
OnNodeDoubleClick / OnNodeContextMenu |
onNodeDragStart / onNodeDragStop |
OnNodeDragStart / OnNodeDragStop |
onEdgeDoubleClick |
OnEdgeDoubleClick |
onPaneClick / onPaneContextMenu |
OnPaneClick / OnPaneContextMenu |
onSelectionChange |
OnSelectionChange(IReadOnlyList<string> nodeIds) |
onConnectStart / onConnectEnd |
OnConnectStart(nodeId, portId) / OnConnectEnd |
onNodesChange / applyNodeChanges |
OnNodesChange / ReactorFlow.ApplyNodeChanges |
onEdgesChange / applyEdgeChanges |
OnEdgesChange / ReactorFlow.ApplyEdgeChanges |
getConnectedEdges(node, edges) |
ReactorFlow.GetConnectedEdges(nodeId, edges) |
screenToFlowPosition / flowToScreenPosition |
ReactorFlow.ScreenToFlowPosition / FlowToScreenPosition |
onReconnect / reconnectEdge |
OnReconnect / ReactorFlow.ReconnectEdge(edgeId, connection, edges) |
toObject() / layout-persistence recipe |
ctx.UsePersistedFlowLayout(persisted, baseNodes, onCommit, seed?) |
edgeTypes |
EdgeTypes (Func<ReactorFlowEdgeContext, Element> per key) |
<BaseEdge /> |
ReactorFlow.BaseEdge(ctx, …) |
<EdgeLabelRenderer /> |
ReactorFlow.EdgeLabelRenderer(ctx, content, offsetX?, offsetY?) |
getBezierPath / getSmoothStepPath / getStraightPath |
ReactorFlow.GetBezierPath / GetSmoothStepPath / GetStepPath / GetStraightPath |
getIntersectingNodes / isNodeIntersecting |
ReactorFlow.GetIntersectingNodes / IsNodeIntersecting |
colorMode="light" / "dark" / "system" |
ReactorFlowOptions.ColorMode |
MarkerType.Arrow / ArrowClosed |
ReactorFlowEdgeMarker on ReactorFlowEdge.MarkerStart / MarkerEnd |
snapToGrid / snapGrid |
ReactorFlowOptions.SnapToGrid / SnapGridSize |
onlyRenderVisibleElements |
ReactorFlowOptions.OnlyRenderVisibleElements |
parentId (sub-flows) |
ReactorFlowNode.ParentId |
<NodeResizer /> / <NodeToolbar /> |
ReactorFlow.NodeResizer / ReactorFlow.NodeToolbar |
<Background /> |
ReactorFlow.Background(variant, gap, size, color) |
<MiniMap pannable zoomable /> |
ReactorFlow.MiniMap(corner, width, margin) or ReactorFlow.MiniMap(ReactorFlowMiniMapOptions) |
<Controls /> |
ReactorFlow.Controls(corner, showZoom, showFitView, margin) |
<Panel position> |
ReactorFlow.Panel(corner, content, margin) |
fitView() |
ReactorFlow.FitView(...), or the Fit button in Controls |
Nodes are data; renderers are components
A node is a plain record. How it looks is decided by a node type renderer — an Element factory keyed by ReactorFlowNode.Type. Because a renderer returns any Element, a node can be a full Reactor component with its own hooks and local state:
var nodeTypes = new Dictionary<string, Func<ReactorFlowNodeContext, Element>>
{
["counter"] = ctx => Component<CounterNode, CounterNodeProps>(new(ctx)),
};
Everything is opt-in
There is no default chrome. A bare ReactorFlow is just nodes, edges and a pan/zoom pane. Add Background, MiniMap, Controls or arbitrary Panel content only when you want them — pass them via Background: (bottom layer) and Overlays: (corner-anchored top layer).
MiniMap interactivity
ReactorFlow.MiniMap() is a static overview by default. Pass ReactorFlowMiniMapOptions to opt into interaction and styling — every flag defaults off, so the plain call is unchanged:
ReactorFlow.MiniMap(new ReactorFlowMiniMapOptions(
ReactorFlowCorner.BottomRight,
Pannable: true, // drag the map to pan the surface (React Flow's `pannable`)
Zoomable: true, // wheel over the map to zoom, clamped to Min/MaxZoom (`zoomable`)
NodesDraggable: true, // drag nodes from the map — emits the same PositionChange stream
NodeColor: n => TintByType(n.Type), // React Flow's `nodeColor`
NodeScale: n => n.Type == "output" ? 1.4 : 1.0)) // enlarge emphasized nodes in the overview
Pannable recenters the surface on the point under the cursor; Zoomable zooms about the surface center within the surface's MinZoom / MaxZoom. NodesDraggable is a community extension (not in React Flow) whose drags flow through the normal OnNodesChange / ApplyNodeChanges path, so UsePersistedFlowLayout persists them too. NodeColor / NodeStrokeColor restyle node rects, NodeScale scales each rect about its center to emphasize nodes, and setting MaskColor swaps the accent viewport box for React Flow's dim off-screen mask.
Handles / ports
Attach ports to a node via ReactorFlowNode.Ports (one per side, or several with Alignment), or place ReactorFlow.Handle(position) inside custom node content. When OnConnect is supplied, dragging from a port draws a live connection and snaps to the nearest target port within ReactorFlowOptions.ConnectThreshold.
Custom port content
Give a port a Type and register a renderer under that key in ReactorFlowProps.PortTypes — the same type -> renderer shape as NodeTypes / EdgeTypes — to replace the default dot with any content (a labeled chip, a status badge, an icon). ReactorFlow draws the returned element centered on the port's anchor point for any side or alignment, so the custom content and the edge endpoint always coincide, and a pointer press still starts a drag-to-connect when OnConnect is wired. Ports without a matching renderer keep the default themed dot.
var portTypes = new Dictionary<string, Func<ReactorFlowPortContext, Element>>
{
["labeled"] = ctx => Border(
TextBlock(ctx.Data?.ToString() ?? ctx.Id).Margin(8, 2))
.CornerRadius(9),
};
// node port: new ReactorFlowPort("out", ReactorFlowPosition.Right, Type: "labeled", Data: "sum")
// props: new ReactorFlowProps(..., PortTypes: portTypes)
Edges
Edges are drawn as native PathGeometry — no external connector library:
ReactorFlowEdgeKind.Default— cubic bezierReactorFlowEdgeKind.SmoothStep— orthogonal with rounded cornersReactorFlowEdgeKind.Step— orthogonal, square cornersReactorFlowEdgeKind.Straight— direct line
Each edge supports a Label, a Color, StrokeWidth, and Animated marching-ants. Only animated edges re-render on the animation tick.
Endpoint arrowheads are set per edge through MarkerStart / MarkerEnd (ReactorFlowEdgeMarker.None / Arrow / ArrowClosed / Dot); the default edge ends in an Arrow.
Custom edges
Register a renderer per ReactorFlowEdge.Type via EdgeTypes — the React Flow edgeTypes shape. A renderer receives a ReactorFlowEdgeContext (resolved endpoints, sides, selection, theme, and Center) and returns any Element. Draw the standard path with ReactorFlow.BaseEdge(ctx, …) (React Flow's <BaseEdge>) and add your own label or button at ctx.Center:
var edgeTypes = new Dictionary<string, Func<ReactorFlowEdgeContext, Element>>
{
["button"] = ctx => Canvas(
ReactorFlow.BaseEdge(ctx),
Button("+", () => AddNodeOnEdge(ctx.Id)).Canvas(ctx.Center.X, ctx.Center.Y)),
};
Reconnecting edges
Supply OnReconnect (and leave ReactorFlowOptions.EdgesReconnectable on) to let users drag a selected edge's endpoint onto a different port. The handler receives the edge id and the new ReactorFlowConnection; apply it with the pure ReactorFlow.ReconnectEdge(edgeId, connection, edges) helper.
Events
Beyond OnNodeClick / OnEdgeClick, ReactorFlow raises the full React Flow interaction surface: OnNodeDoubleClick, OnNodeContextMenu, OnNodeDragStart / OnNodeDragStop, OnEdgeDoubleClick, OnPaneClick / OnPaneContextMenu, OnSelectionChange (the current selected-node id list), and OnConnectStart / OnConnectEnd around a drag-to-connect gesture. Dragging any node in a multi-selection moves the whole selection together.
Coordinate helpers
ReactorFlow.ScreenToFlowPosition(point, viewport) and FlowToScreenPosition(point, viewport) convert between pane-relative screen points and world/flow coordinates — the React Flow screenToFlowPosition / flowToScreenPosition pair — for hit-testing, drop targets, or placing new nodes under the pointer.
Deriving edges from ports
Instead of maintaining a separate edge list, you can declare a node's outgoing connections on its ports and let ReactorFlow build the edges. Add ReactorFlowPortLink(target, targetPort, label?, animated?) entries to a ReactorFlowPort.Links, then call ReactorFlow.DeriveEdges(nodes):
var nodes = new List<ReactorFlowNode>
{
new("a", 0, 0, 120, 60, Ports: new[]
{
new ReactorFlowPort("out", ReactorFlowPosition.Right,
Links: new[] { new ReactorFlowPortLink("b", "in", Label: "next") }),
}),
new("b", 240, 0, 120, 60, Ports: new[] { new ReactorFlowPort("in", ReactorFlowPosition.Left) }),
};
var edges = ReactorFlow.DeriveEdges(nodes); // one edge: "a:out->b:in"
Edge ids are deterministic ("{node}:{port}->{target}:{targetPort}") and each edge is wired source-port to target-port, so it routes and anchors exactly like an authored edge.
Persisting layout
ctx.UsePersistedFlowLayout(persisted, baseNodes, onCommit, seed?) restores and persists just the runtime-mutable layout — every node's position and size plus the viewport — so the exact arrangement replays later on the same or another machine. Everything else about a node (type, data, ports, parent) is reconstructed from baseNodes and is not persisted. Spread the result onto ReactorFlowProps:
var layout = ctx.UsePersistedFlowLayout(
persisted: loadedBlob, // JSON string previously stored, or null
baseNodes: seedNodes, // authoritative graph (memoized)
onCommit: blob => Save(blob), // called on drag-stop / resize / pan-zoom
seed: n => (n.X, n.Y)); // placement for nodes new to the blob
return Component<ReactorFlow, ReactorFlowProps>(new(
Nodes: layout.Nodes,
Edges: edges,
Viewport: layout.Viewport,
OnNodesChange: layout.OnNodesChange,
OnViewportChange: layout.OnViewportChange));
Commits fire when a drag ends (not on every intermediate frame). The hook is a RenderContext extension, so call it from a function component (Func(ctx => …)) or forward a render context.
Selection & editing
Nodes and edges are selectable and deletable out of the box:
- Click a node or edge to select it; the selection is highlighted. Edge clicks report through
OnEdgeClickand are hit-tested via a transparent hit path, so thin edges are still easy to click. - <kbd>Shift</kbd>+drag on the pane rubber-bands a marquee multi-select; <kbd>Shift</kbd>+click toggles a node in the selection (
SelectionOnDragmakes a plain drag select). - Press <kbd>Delete</kbd> or <kbd>Backspace</kbd> to remove the selection. Deleting a node cascades to its connected edges — the removals are emitted through
OnNodesChange/OnEdgesChange, so wire both (withApplyNodeChanges/ApplyEdgeChanges) to keep your state in sync. - Reject invalid connections before they commit with
IsValidConnection— returnfalseto veto a drag-to-connect gesture beforeOnConnectfires. - Toggle behavior globally through
ReactorFlowOptions:NodesDraggable,NodesSelectable,EdgesSelectable,ElementsDeletable, andFitViewOnInit.
Sub-flows, resizing & big graphs
- Grouping — set
ReactorFlowNode.ParentIdto nest a node inside another; its position becomes relative and it moves with the parent.ReactorFlow.GetAbsolutePositionflattens the chain. - NodeResizer / NodeToolbar — compose
ReactorFlow.NodeResizer(ctx, …)andReactorFlow.NodeToolbar(ctx, …)inside aCanvas-based custom node for corner resize handles and a floating toolbar shown while selected. - Snap & guides —
SnapToGrid/SnapGridSizesnap dragged nodes to a grid;ShowAlignmentGuidessnaps to neighbors' edges/centers and draws guide lines. - Virtualization —
OnlyRenderVisibleElementsculls off-screen nodes/edges for large graphs (ReactorFlow.GetVisibleNodesexposes it as a pure helper).
See the docs for full guides and the API reference.
Options
ReactorFlowOptions controls zoom limits, pan/zoom gestures, handle sizes, corner radius, the connect snap threshold, snap-to-grid, alignment guides, virtualization, and the interaction toggles above. Pass it via ReactorFlowProps.Options.
Sample
The sample app is a self-contained board with several node types (including an interactive stateful counter, a sub-flow group with child nodes, and a resizable node with a NodeToolbar + NodeResizer), ports on all four sides including a labeled-chip custom PortTypes handle, labeled / smooth-step / animated edges with different end markers, a custom EdgeTypes "button" edge (built on BaseEdge) with an inline delete button, drag-to-connect, edge reconnection, marquee multi-select and group multi-drag, alignment guides, pan/zoom, a live event read-out wired to the full event surface, and the Background / MiniMap (pannable, zoomable, node-draggable, type-tinted, with NodeScale emphasis) / Controls / Panel chrome.
dotnet run --project samples/Reactor.Community.ReactorFlow.Sample
License
MIT © 2026 Reactor Community
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-windows10.0.26100 is compatible. |
-
net10.0-windows10.0.26100
- Microsoft.UI.Reactor (>= 0.1.0-preview.11)
- Microsoft.WindowsAppSDK (>= 2.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.0-preview.2 | 36 | 7/2/2026 |
| 0.4.0-preview.1 | 38 | 7/2/2026 |
| 0.1.0-preview.1 | 36 | 7/2/2026 |