Blog / Use cases
Sub-flows, groups, containers: why nesting breaks every flow library
Look at the top-voted feature requests of any flow library and one theme towers over the rest: users want to put things inside other things. In React Flow's tracker it's "Nested flows", "Nested Nodes" and "absolute positioning in sub flows" — sixty-plus upvotes between them, one still open. This isn't a React Flow problem. It's that nesting looks like a styling feature and is actually a semantics feature, and half-shipping it is worse than not shipping it.
Why it's genuinely hard
The moment a node can contain nodes, every subsystem gets a second opinion:
- Dragging: does moving the parent move the children? (Yes.) Does dragging a child near the edge leave the parent? (Your call — but it must be a call, not an accident. One of the top-voted react-flow Stack Overflow questions is literally "one node drags other nodes" — accidental containment.)
- Hit-testing: a click inside a container over empty space — who gets it? The container? The canvas? The deepest nested thing under the pointer?
- Serialization: if membership isn't part of the document, a saved diagram reloads as loose boxes over a decorative rectangle.
- Undo: dragging a node from one container into another touches two containers and a membership record. One gesture must be one undo step.
- Collapse: fold a container and its links must re-anchor to the header — and unfold must restore exactly what was there, even after a save/reload.
Libraries that treat groups as a rendering trick get the first demo right and the next four bullets wrong — which is exactly why the feature requests stay open for years. The correct-but-expensive answer is that containment has to live in the model.
Containment is membership
In Grafloria a group is a model object with members, and everything else follows from that one decision:
import { GroupModel } from '@grafloria/engine';
diagram.addGroup(new GroupModel({ id: 'ingest-stage', position: { x: 0, y: 0 },
size: { width: 320, height: 180 } }));
await engine.addToGroup('ingest-stage', 'a'); // membership is model state
await engine.collapseGroup('ingest-stage'); // reversible: a snapshot rides ON the group
await engine.expandGroup('ingest-stage'); // …so collapsed diagrams survive save/reload
Members drag with the group. Collapse stores member positions and removed links on the group itself, so a collapsed diagram serializes and expands correctly in a different session. Hit-testing is deepest-wins. And because group operations are commands, the cross-container drag really is one ⌘Z.
The dashboard version, since that's where nesting usually ends up
The most common concrete ask behind "nested flows" is a dashboard section: a box of KPIs that lays out its own children and accepts tiles dragged in. In our dashboard kit that's one field on a widget:
{ id: 'kpis', title: 'KPI section', span: 12, columns: 4,
widgets: [
{ id: 'k1', kind: 'kpi', span: 1, data: { label: 'Revenue', value: '$6.8M' } },
{ id: 'k2', kind: 'kpi', span: 1, data: { label: 'Customers', value: '1,284' } },
] }
Drag a tile across the boundary and the other grid adopts it live; drag it back out
and the board takes it back; either way one undo restores everything, and
toJSON() reports the tile under whichever parent it's actually in —
derived from live membership, not from the array you authored. Resize a child taller
than the section and the section grows a row in its board rather than letting
the child overflow.
Try both live: group demos · dashboard containers · the concepts: Groups.