A generated architecture is useful only if you can challenge it. Move a boundary, rename a service, undo a bad idea. Codelit's canvas is where a model's suggestion becomes something an engineer can work with.
is a React library for node-based interfaces that renders every node as a real React component, so a diagram node can hold a form, an icon, or a live status badge instead of being a rectangle drawn onto a bitmap.
I spent more time on the canvas than on any other part of the product, which is fitting given that the whole thing started as a Lucidchart rage-quit. React Flow (@xyflow/react) made it possible without me having to build a rendering engine from scratch. But "possible" and "easy" are very different words.
Why React Flow instead of D3, Cytoscape, or raw canvas?
React Flow won because I needed an editor made from React components, with selection and connection behavior already in place. Other libraries can support rich interactions, but React Flow reduced the amount of integration code between the graph and the rest of the application.
| Option | How nodes render | What it would have cost me | Verdict |
|---|---|---|---|
| Raw HTML5 Canvas | Pixels I draw myself | Zoom, pan, marquee selection, keyboard navigation, hit testing on overlapping nodes, accessibility | A multi-month project for a team, not a weekend hack for a solo dev |
| D3.js | Flexible data and rendering primitives | Designing selection, connection editing, and React ownership boundaries | More low-level control than this editor needed |
| Cytoscape.js | Canvas-based graph visualization | Integrating rich React controls with the graph surface | Worth considering for graph analysis; less direct for my form-heavy nodes |
React Flow (@xyflow/react) | Real React components | Bundle size, and layout is still entirely your problem | Shipped |
I did prototype the raw canvas version over a weekend. Drawing rectangles and lines was fine. Then I started listing what still had to exist before anyone could actually use the thing, and killed it.
The rest of the case for React Flow is unglamorous and decisive: pan, zoom, minimap, selection and keyboard shortcuts work out of the box; every node type, edge type and event handler is properly typed; and the maintainers at xyflow are active. I've opened 3 issues and all got responses within a day.
What goes inside a custom node?
A Codelit node is a React component that renders an icon, a label, a metadata line, and the handles edges connect to. Everything it shows comes from the node's data object, including the status that decides whether it draws as healthy, degraded, or failed.
The example below shows a database node. Its status is diagram state, not a live health measurement unless a separate data source supplies one. The component can explain a simulation without pretending to monitor production.
That status field is what chaos mode drives when it kills a component and models how failure could spread. The visual state is useful because it is inspectable, not because it proves a real outage.
The current Codelit canvas registers one shared systemNode component and passes the infrastructure role through its data. Separate components are another valid design; the simplified example below shows how a larger library could register them.
Registering custom nodes with React Flow is straightforward:
How do you show sync and async traffic on the same canvas?
Codelit encodes traffic type in the edge itself: blue solid for synchronous HTTP calls, amber dashed for async queue messages, green dotted for pub/sub events. Users can tell at a glance which connections are sync and which are async without reading a single label.
Motion can suggest direction, but it must not imply observed traffic when the diagram has no telemetry source. This simplified edge treatment uses CSS rather than JavaScript per frame, and should respect reduced-motion preferences:
Why is layout harder than drawing nodes?
The AI returns a JSON structure describing components and their connections, and it knows nothing about pixel coordinates, visual spacing, or where anything belongs on a 1200x800 canvas. Converting that into a well-laid-out diagram was the hardest problem in the entire product.
Nothing upstream helps. The pipeline that produces that JSON deals in components and edges, not geometry, and the same problem lands on you whether the nodes arrived from a prompt or from Mermaid syntax pasted into the editor.
I explored several layout approaches. Their tradeoffs matter more than a rewrite count:
| Attempt | Layout approach | What happened |
|---|---|---|
| 1 | Grid rows and columns | Looked like a spreadsheet. No visual hierarchy. Killed it |
| 2 | Force-directed (d3-force) | Nodes overlapped constantly and the layout was non-deterministic. Same input, different positions every time. Users hated that |
| 3 | Random with collision avoidance | Worse than the grid. Don't ask |
| 4 | Manual heuristics: load balancers top, services middle, databases bottom | Worked for simple architectures, completely fell apart for anything with more than 10 nodes |
| 5 | Dagre, default config | The breakthrough. Layouts finally looked like real architecture diagrams, but the default spacing was too tight and edge crossings were common |
| 6 | Dagre with a custom config | An option when the graph needs more layout control |
The current architecture canvas uses a smaller depth-based layout: find roots, walk outgoing edges with cycle bounds, assign fallback rows by type, and center each row. The concrete payoff is a predictable diagram without making layout another remote request.
Dagre is an alternative when a layered graph needs more control over ordering and spacing. This example shows its coordinate conversion; it is not the current Codelit layout implementation.
How do you make every canvas edit undoable?
Codelit's current version store records architecture objects with their prompt and timestamp, then tracks the active index. Adding a version removes any redo branch beyond that index. This makes supported domain edits recoverable without tying the history model to React Flow's rendering state.
The equivalent past-present-future pattern is useful to understand:
In that example, a CanvasSnapshot must be an immutable copy of the state you intend to restore. Group drag events into meaningful edits if positions belong in history. Neither a version array nor a toolbar button proves that every gesture is captured; test each supported edit.
It's the least glamorous code in the product, in the same category as the ⌘K command palette: nobody praises it, everybody notices its absence.
What actually makes a React Flow canvas slow?
Unnecessary re-renders are one common bottleneck, alongside expensive node markup, many edges, and complex styling. React Flow's performance guidance recommends stable components, callbacks, and narrow subscriptions. Profile the interaction before deciding which cost dominates your canvas.
Memoize custom nodes. Keep node components and the nodeTypes object stable. Start with React.memo; only add a custom comparator when profiling justifies it, and compare every prop that affects rendering or behavior.
Batch state updates. When the AI generates a 30-node diagram, I don't add nodes one at a time, which would trigger 30 re-renders. I set the entire node array in one setState call.
Lazy load the canvas. React Flow isn't a tiny dependency, so I dynamically import it and keep it off the initial page load for users who land on the marketing page. That discipline matters more once the app is installed as a PWA and opened cold, where there's no browser chrome to hide a slow start behind.
Measure representative graphs. Compare drag, zoom, selection, and editing on a low-end device as well as your development machine. Offscreen rendering can be configured, but should not be described as automatic virtualization without checking the actual setting. Node count alone is not a benchmark.
Where can you try the canvas yourself?
The best way to understand the canvas is to use it. Open Architecture in Codelit, describe a system, and inspect the result. Drag nodes around. Click edges. Try the minimap. Or start from a pre-built template and modify it.
If you're building your own node-based UI with React Flow, I hope this saves you some of the trial and error I went through. The library removes a large amount of editor plumbing. The product work remains yours: preserve intent, make edits reversible, and keep a dense diagram understandable. Those are the details people feel every time they use it.
Questions people actually ask
- Should I use React Flow or D3 to build a diagram editor?
- React Flow is a strong fit when nodes contain forms, menus, or status indicators and the editor needs selection, dragging, and connections. D3 also works with React when responsibilities are separated, but it leaves more editor behavior to you. Choose for the interaction model you need, not a claim that one library cannot draw the other.
- How do you automatically position nodes in a React Flow diagram?
- React Flow does not lay out nodes for you. It renders whatever coordinates you hand it, so positioning belongs to the application. Codelit's current architecture canvas computes depth from root nodes, groups nodes into rows, and uses type-based fallbacks for unvisited nodes. Libraries such as Dagre are another option when a graph needs more sophisticated automatic layout.
- Why does force-directed layout fail for architecture diagrams?
- Force-directed layout can work well for network exploration, but my implementation did not preserve the stable hierarchy I wanted for architecture diagrams. Layered layout was a better fit for Codelit. Determinism depends on initialization and configuration; non-determinism is not an unavoidable property of every force simulation.
- How many nodes can React Flow handle before it gets slow?
- There is no universal node limit. Performance depends on node complexity, edge count, styles, subscriptions, device, and interaction. Profile representative graphs while dragging and zooming. Stable component references, memoization, and narrow state subscriptions help, but a frame-rate claim is only meaningful with a device and test scenario attached.
- Can you add undo and redo to a React Flow canvas?
- Yes. Keep versions of the domain graph and track the active version. Codelit's Zustand version store truncates the redo branch when a new architecture version is added, then moves an index for undo and redo. A past-present-future stack is an equivalent teaching pattern, but does not automatically make every UI action undoable.