Skip to main content
Version: 4.3

Accessing State

In @joint/react, graph state lives inside GraphProvider.

You usually read it at two levels:

  • graph-wide collections with useCells()
  • the current render context with useCell()

This chapter focuses on those two levels and how they fit together. For the underlying record shapes, see Elements and Links.

The example below is a small workflow diagram with an overlay inspector panel.

The inspector panel reads the whole graph from anywhere inside GraphProvider: it uses useCells with selectors to derive counts, IDs, and route summaries. Each task node reads only its own data because it is rendered inside Paper.renderElement. Try dragging a node; both the card and the panel update reactively.

Graph-wide reads

Use useCells() when a component needs the graph as a whole.

Without arguments, it returns an array of all cells (elements and links combined):

const cells = useCells();

That array shape is intentional:

  • each cell carries its own id and type ('element' or 'link')
  • iteration is direct (insertion-ordered, but order is not stable across deletions, so do not rely on positional index identity)
  • components can derive just the subset they need with selectors

useCells() supports two patterns:

// full array
const cells = useCells();

// selector mode: pair with `isElement` / `isLink` from `useGraph()` so custom shape subclasses register correctly
const { isElement, isLink } = useGraph();

const elementCount = useCells((items) => items.filter(isElement).length);
const linkIds = useCells((items) => items.filter(isLink).map((link) => link.id));

Selector mode is the most useful pattern for sidebars, inspectors, counters, and derived summaries. It helps keep re-renders focused on the specific slice you actually use.

By default, selectors that return arrays are compared shallowly, so cells.map((c) => c.id) does not fire spurious re-renders. For custom equality on objects or deep structures, pass an equality function as the second argument.

Stable selector returns

In development, useCells warns when a selector returns a fresh array of new object references that are shallow-equal to the previous result, for example useCells((cells) => cells.map((c) => ({ ...c }))). Each call produces unequal references with the same values, so every commit triggers a re-render even when nothing changed.

Fix by returning primitives (cells.map((c) => c.id)), returning the cell records directly (cells.filter(predicate)), or passing a custom isEqual as the second argument.

useCells also has id-targeted overloads that subscribe only to a known cell or set of cells:

const cell = useCells(id);            // single cell by id
const subset = useCells(ids); // fixed set of ids
const record = useCells<ElementRecord<NodeData>>(id); // record?.data is typed

These avoid filtering the whole array when the target ids are already known. ElementRecord is a type import from @joint/react; typing the single-id read this way makes record?.data resolve to NodeData.

Tracking a collection

Pass an mvc.Collection<dia.Cell> to scope useCells to that collection's membership instead of the whole graph. The hook reacts to the collection's add, remove, and reset events and reads cell records from the surrounding GraphProvider; cells that are not currently in the graph are skipped.

import type { dia, mvc } from '@joint/core';

function SelectionPanel({ selection }: { selection: mvc.Collection<dia.Cell> }) {
const selected = useCells(selection);
return <span>{selected.length} selected</span>;
}

This is the same set of overloads as the id-list form; pair it with a selector and optional equality function when you only need a derived slice.

Contextual reads

Use useCell() inside the render tree of the current cell. It works in both renderElement and renderLink; the surrounding context tells the hook which cell to subscribe to.

Pass a selector to subscribe to a single slice. Each call re-runs only when its slice changes, so node updates stay focused.

Each node above subscribes to its own data, position, size, and angle through predefined selectors. Drag either to watch the values update live.

import {
useCell,
selectElementPosition,
selectElementSize,
selectElementData,
} from '@joint/react';

const { label } = useCell(selectElementData<NodeData>);
const position = useCell(selectElementPosition);
const size = useCell(selectElementSize);

@joint/react ships predefined selectors for the common slices.

Predefined selectors

These selectors are designed to pass into useCell(). They also compose inside a useCells() selector when mapping over the array:

Element-only (require an element record):

SelectorReturns
selectElementPositionelement.position
selectElementSizeelement.size
selectElementAngleelement.angle
selectElementData<D>element.data typed as D

Cell-level (work for both elements and links):

SelectorReturns
selectCellIdcell.id (inside renderElement/renderLink, useCellId() is cheaper for the current cell's id)
selectCellTypecell.type, typed as 'element' | 'link'. At runtime a built-in shape carries its full type string (e.g. 'standard.Rectangle'), but the declared return narrows to the two literals; use useGraph().isElement(cell) / isLink(cell) to classify reliably
selectCellParentcell.parent (or null for top-level cells)

Reading the whole record

When most fields are actually used, you can skip selectors and read the whole record with a typed cell shape:

import { useCell, type ElementRecord, type Computed } from '@joint/react';

type NodeRecord = Computed<ElementRecord<NodeData>>;

const { data, position, size, angle } = useCell<NodeRecord>();

The bare hook subscribes to the whole record, so it re-renders on any field change. Reach for it only when the renderer really reads everything; otherwise prefer selectors.

renderLink receives the link's data as a prop, the same way renderElement does. When the renderer also needs the rendered path, for example to position an overlay at the link's midpoint, read the geometry with useLinkLayout().

useLinkLayout() returns LinkLayout | undefined. Guard with if (!layout) return null; while the link view is still mounting or its source/target are not yet positioned. When defined, it carries:

  • layout.sourceX
  • layout.sourceY
  • layout.targetX
  • layout.targetY
  • layout.d

useCell() is also available inside renderLink when the renderer needs other fields from the link record (source, target, custom props), but the simpler reads (data from props, geometry from useLinkLayout()) cover most overlays.

The example below renders a status badge at each link's midpoint. Drag a node and watch them track the path:

useLinkLayout() is resolved per paper. If the same graph is rendered in multiple papers, the link record is shared but the geometry can differ from paper to paper.

Experimental

renderLink and useLinkLayout are experimental. The API may change in future releases.

Quick reference

Graph-wide

  • useCells(): returns a full array of cells, or a selected value. Works in any child of GraphProvider. Filter by type === 'element' or type === 'link' to narrow to elements or links.

Contextual

  • useCell(): returns the current cell record. Accepts an optional selector for narrower subscriptions. Works inside renderElement and renderLink.
  • useCell(id) / useCell(id, selector): subscribe to a specific cell by id from anywhere; throws if the id does not resolve.
  • useLinkLayout(): returns the current link's rendered geometry on the surrounding paper, or undefined while still mounting. Works inside renderLink.

Convenience

  • useCellId(): reads the current cell's ID from context. Works inside renderElement and renderLink.

For the full list of predefined selectors, see Predefined Selectors above.