Updating State
Once you can read graph state, the next step is changing it.
In @joint/react, the main action hook is useGraph(). It gives you:
- the underlying JointJS
graph(escape hatch, see below) setCell(): add or update a cell (setCell(record)direct upsert, orsetCell(id, updater)typed update)setCellData(): replace or update just a cell'sdata(setCellData(id, data)orsetCellData(id, updater))removeCell()/removeCells(): remove cellsresetCells(): replace all cells at onceupdateCells(): apply a transform to the whole collectionisElement()/isLink(): type guards over the unified cell stream
Use it from any component inside GraphProvider.
The demo below is a small pipeline builder.
The controls create and remove pipeline cells with setCell() and removeCell(). Each node updates its own data.status with setCellData() when clicked. Neither component talks to JointJS views directly. They update graph state through useGraph(), and Paper reflects those changes automatically.
useGraph()
Use useGraph() when a component needs to modify cells programmatically.
const {
graph,
setCell,
setCellData,
removeCell,
removeCells,
resetCells,
updateCells,
isElement,
isLink,
} = useGraph();
The common operations are:
setCell(record): direct upsert byid(record must includeidandtype)setCell(id, updater): update an existing cell by id; the updater receives the current record and returns the next onesetCellData(id, data): replace the cell'sdatawholesalesetCellData(id, updater): update the cell'sdata; the updater receives the currentdataand returns the nextremoveCell(id): remove a single cellremoveCells(ids): remove multiple cellsresetCells(recordsOrUpdater): replace all cells at onceupdateCells(updater): apply a transform to the whole collection
setCell() is an upsert: with the direct form, if a cell with the given id exists the input merges over it; otherwise the cell is added. The input must include id and type ('element' or 'link'). The updater form requires the cell to already exist; if it does not, the call warns in development and is ignored. Use the direct form to add a new cell.
Adding cells
Use setCell() with a fresh id to insert a new cell into the graph. Pass an object with id, type, and the relevant fields:
setCell({
id: 'review',
type: 'element',
position: { x: 270, y: 140 },
data: { label: 'Review' },
});
setCell({
id: 'review-output',
type: 'link',
source: { id: 'review' },
target: { id: 'output' },
style: { targetMarker: 'arrow' },
});
Because the same call updates an existing cell, you can rerun the snippet without worrying about duplicates; the second call merges over the first.
Direct updates vs updater functions
setCell() supports two forms.
Direct record updates
Use the direct object form when you already know the full value you want to change. The record must include id and type:
setCell({
id: 'review',
type: 'element',
position: { x: 270, y: 140 },
});
setCell({
id: 'review-output',
type: 'link',
source: { id: 'review' },
target: { id: 'output' },
style: { targetMarker: 'arrow' },
});
This form merges at the record level, so it is good for top-level fields such as position, size, source, target, and style.
Updater functions
Use the updater form when the new value depends on the previous one, especially for nested objects like data or style. Pass the target id as the first argument; the updater receives the current record. The cell must already exist; to insert a new cell, use the direct record form.
The updater's previous is the union of element and link records. Here the target 'review-output' is a link, so its style shorthand (which @joint/react expands into native attrs) can be updated in place:
setCell('review-output', (previous) => ({
...previous,
style: { ...previous.style, color: '#2563eb', width: 3 },
}));
For typed access to previous.data, parameterise useGraph() with the record shape and narrow the union with isElement (or isLink):
import { useGraph, type ElementRecord } from '@joint/react';
const { setCell, isElement } = useGraph<ElementRecord<StepData>>();
setCell('step-1', (previous) => {
if (!isElement(previous)) return previous;
return {
...previous,
data: { ...previous.data, status: 'done' },
};
});
Inside the isElement branch, previous narrows to ElementRecord<StepData>, so previous.data is StepData. The generic is record-shaped (ElementRecord<MyData> / LinkRecord<MyData>), not data-shaped.
Updating a cell's data
When a change only touches a cell's data, setCellData is more direct than setCell. It targets the data field by id, so you skip spreading the whole record just to reach data.
const { setCellData } = useGraph<ElementRecord<StepData>>();
// Replace the data wholesale.
setCellData('step-1', { label: 'Review', index: 1, status: 'done' });
// Or derive it from the current data.
setCellData('step-1', (data) => ({ ...data, status: 'done' }));
The updater receives the current data, typed from the useGraph() generic, and returns the next data. The direct form replaces data wholesale, so use the updater when you want to keep the other fields. As with setCell, a nullish id or an id with no matching cell warns in development and is ignored.
The status badge in the demo above cycles a node's status with setCellData.
Replacing or transforming all cells
For batch operations on the whole collection, useGraph() returns two helpers: resetCells() and updateCells(). resetCells() takes an array or an updater function; updateCells() takes an updater function that receives the current cells. They also differ in how the paper reacts.
resetCells() is a true replace. JointJS destroys and recreates every element view, so external references tied to the old views (custom views, tool handles, drag clones) become stale. Reach for it when the new state is unrelated to the old one, such as loading a saved diagram, switching presets, or resetting to the initial state:
const { resetCells } = useGraph();
resetCells([
{
id: 'source',
type: 'element',
position: { x: 40, y: 60 },
size: { width: 140, height: 56 },
data: { label: 'Source', index: 0, status: 'idle' },
},
]);
updateCells() reconciles the existing collection. Cells that survive keep their views; only added or removed cells touch the DOM. Reach for it when transforming the current graph in place (filtering, mapping, bulk migrations):
const { updateCells, isElement } = useGraph<ElementRecord<{ status?: string }>>();
updateCells((previous) =>
previous.filter((cell) => !isElement(cell) || cell.data.status !== 'done')
);
For updates to individual cells, prefer setCell(). The batch operations are designed for cases where you need to replace or transform the whole collection at once.
The raw graph instance
useGraph() also returns the underlying JointJS graph instance.
That is the escape hatch for advanced Graph APIs such as:
graph.getCell(id)graph.getNeighbors(cell)graph.getConnectedLinks(cell)
Use that path when you need native JointJS behavior that is outside the React convenience layer. For a fuller Graph API walkthrough, continue to Querying the Graph.
For everyday React app updates, prefer setCell() and removeCell().
Controlled-mode note
In uncontrolled mode, useGraph() is usually the main update path.
In controlled mode, React or your external store owns the long-lived state. useGraph() still works inside that setup, but the real source of truth is the state you pass back into GraphProvider.
Quick reference
Mutations
setCell(record): upserts a cell byid. Adds the cell when the id is unknown; merges over the existing cell when the id matches.setCell(id, updater): applies the updater to the existing cell with the given id. When no such cell exists, it warns in development and no-ops; use the directsetCell(record)form to add a new cell.removeCell(id): removes a cell by ID.removeCells(ids): removes multiple cells.resetCells(recordsOrUpdater): replaces all cells. Accepts an array or an updater function.updateCells(updater): applies a transform over the full cell collection.
Type guards
isElement(input):truewhen the input resolves to an element cell (consults the graph's type registry, so custom shapes are recognised).isLink(input):truewhen the input resolves to a link cell.
Escape hatch
graph: the underlying JointJS graph instance for advanced APIs likegetNeighbors()andgetConnectedLinks().