Skip to main content
Version: 4.3

Collapse & Expand

In @joint/react, collapse and expand behavior is built from ordinary graph state plus Paper.cellVisibility, so you shape it to fit your data.

The pattern:

  • store UI state such as collapsed in element data
  • store actual visibility on cells with a top-level hidden property
  • filter rendered cells with cellVisibility
  • traverse descendants through the graph instance and update them with useGraph()'s setCell()

This example uses a fixed tree with seven nodes and inline toggle buttons. Click the +/- buttons on branch nodes to collapse and expand subtrees.

How the pattern works​

When a user toggles a branch:

  1. The node reads its own id with useCellId() and calls an onToggle() callback
  2. That callback gets the cell from the graph instance via useGraph()
  3. It finds descendant elements with graph.getSuccessors(cell)
  4. It expands that set into a subtree with graph.getSubgraph([cell, ...successors])
  5. It flips the current node's data.collapsed flag with setCell()
  6. It sets hidden on every descendant cell in that subtree with setCell()
  7. It calls wakeUp() from usePaper() to tell Paper to re-evaluate cellVisibility

The toggled node stays visible. Only its descendants are hidden or shown.

Why cellVisibility matters​

hidden is just state. cellVisibility is what turns that state into visible behavior:

const cellVisibility = ({ model }: { model: dia.Cell }) => !model.get('hidden');

<Paper cellVisibility={cellVisibility} />

After updating hidden on cells, call wakeUp() from usePaper() to tell Paper to re-evaluate cellVisibility for all cells. Auto-layout diagrams get this for free because repositioning triggers view updates, but fixed-position trees need the explicit call.

const { wakeUp } = usePaper();
// ...after setCell() updates...
wakeUp();

This is more efficient than leaving every node rendered and trying to hide them only with CSS, because the hidden cells are skipped by the Paper renderer entirely.

Resetting nested branches​

When a parent branch changes state, the example resets data.collapsed on descendant elements as it shows them again:

setCell(subtreeCell.id, (previous) => {
if (!isElement(previous)) return previous;
return {
...previous,
hidden: false,
data: { ...previous.data, collapsed: false },
};
});

That keeps reopened branches predictable. A parent expand restores the whole subtree to a visible, expanded state instead of preserving older nested toggles.

If your diagram is auto-laid out​

This example uses fixed positions, so collapsing a branch only hides or shows part of the tree.

For auto-laid-out diagrams, rerun layout after toggling if you want the remaining visible nodes to close up around the reduced tree. If your layout is already managed elsewhere, keep the collapse logic the same and treat relayout as a separate step after the subtree visibility update.