Skip to main content
Version: 4.3

Event Handling

@joint/react gives you three ways to react to events:

  • <Paper> event props: handler props directly on <Paper> (onElementPointerClick, onBlankPointerDown, …) for interactions on the paper. The simplest, co-located option.
  • useOnPaperEvents(): the hook form of those same handlers, for subscribing from another component, to events without a prop, or to many events at once.
  • useOnGraphEvents(): for graph and model changes (add, remove, move, batch updates).

All three manage listeners automatically and clean up on unmount.

This editor tracks the clicked node and logs position changes to an overlay panel. Click a node to select it, then drag it and watch the position log update in real time.

The demo handles clicks with <Paper> event props (selecting and deselecting nodes) and listens for graph changes with useOnGraphEvents, which logs every position update as you drag.

Event props on <Paper>

The quickest way to handle an interaction is a handler prop on <Paper>. Each is a camelCase name (onElementPointerClick, onBlankPointerDown, onLinkMouseEnter, and so on) and receives a single context object. It always carries paper, graph, and event; cell, element, and link handlers add id, model, and view, and pointer handlers add x and y (blank and hover handlers carry only the subset that applies).

import { useCallback } from 'react';

function Canvas() {
const handleElementClick = useCallback(({ id }) => setSelectedId(id), []);
const handleBlankClick = useCallback(() => setSelectedId(null), []);

return (
<Paper
onElementPointerClick={handleElementClick}
onBlankPointerClick={handleBlankClick}
/>
);
}

These cover the day-to-day pointer, hover, wheel, and context-menu events on cells, elements, links, and blank areas. The exact per-handler payloads are listed under Common Paper events below.

Handlers can be inline: <Paper> reads your latest handler on every event and does not re-subscribe when a handler's reference changes. It re-attaches listeners only when you add or remove an event handler (toggle a prop on or off) or switch to a different paper.

useOnPaperEvents()

useOnPaperEvents() exposes the same normalized handlers as a hook. Reach for it when a prop on <Paper> won't do: to subscribe from a component that isn't the <Paper> itself (a toolbar, a sidebar), to events that have no prop (raw JointJS lifecycle events), or to attach many handlers from one place.

Called with no target inside a <Paper> subtree, it binds to the surrounding paper from context (falling back to the single default paper):

import { Paper, useOnPaperEvents } from '@joint/react';

function PaperListeners() {
useOnPaperEvents({
onElementPointerClick: ({ id, model, x, y }) => {
// handle element click
},
onBlankPointerClick: ({ x, y }) => {
// handle blank area click
},
});
return null;
}

function Canvas() {
return (
<Paper>
<PaperListeners />
</Paper>
);
}
Targeting a specific paper

useOnPaperEvents() resolves the single default paper automatically. Pass a paper id or ref, as in useOnPaperEvents(paper, handlers), to target a specific paper when your app renders more than one.

The handler map is the only argument: useOnPaperEvents(handlers). There is no dependency array: the hook always runs your latest handlers, so listeners never need to be re-attached.

For events outside the normalized set (advanced lifecycle events like 'render:done', 'cell:highlight', batch events, and so on), drop to raw JointJS event names. Raw handlers keep the native positional-argument signature and live in the same map alongside normalized ones, so you can mix freely.

Common Paper events

Normalized handlers cover the day-to-day interaction events. Their context object always carries paper and graph, so the old "callback factory" form is no longer needed.

Normalized handlerContext payload
onElementPointerClick{ paper, graph, id, model, view, event, x, y }
onElementPointerDblClick{ paper, graph, id, model, view, event, x, y }
onElementMouseEnter{ paper, graph, id, model, view, event }
onElementMouseLeave{ paper, graph, id, model, view, event }
onLinkPointerClick{ paper, graph, id, model, view, event, x, y }
onLinkMouseEnter{ paper, graph, id, model, view, event }
onLinkMouseLeave{ paper, graph, id, model, view, event }
onBlankPointerClick{ paper, graph, event, x, y }
onBlankPointerDown{ paper, graph, event, x, y }
onCellPointerClick{ paper, graph, id, model, view, event, x, y }

The table above is a starter: every cell/element/link/blank event has matching normalized handlers for pointer, hover, wheel, and contextmenu. For the advanced lifecycle events (highlighter, render, layer, batch), drop to the raw JointJS event names.

Accessing graph and paper

Every normalized handler receives graph and paper in its context object, so reach for them directly:

useOnPaperEvents({
onElementPointerClick: ({ graph, model }) => {
const neighbors = graph.getNeighbors(model);
console.log(`${model.id} has ${neighbors.length} neighbors`);
},
});

If you need them outside an event handler, read them with useGraph() and usePaper() from anywhere inside the provider.

useOnGraphEvents()

Use useOnGraphEvents() to listen to JointJS graph events.

The most common form uses the graph from the surrounding GraphProvider:

import { useOnGraphEvents } from '@joint/react';
import type { dia } from '@joint/core';

useOnGraphEvents({
add: (cell: dia.Cell) => {
console.log('cell added:', cell.id);
},
'change:position': (cell: dia.Cell, newPosition) => {
console.log('moved:', cell.id, newPosition);
},
});

You can also pass an explicit graph instance as the first argument. That is useful outside a GraphProvider or when you need to listen to a specific graph:

useOnGraphEvents(graph, {
remove: (cell: dia.Cell) => {
console.log('cell removed:', cell.id);
},
});

Common graph events

EventCallback SignatureFires When
add(cell, collection, opt)Cell added to the graph
remove(cell, collection, opt)Cell removed from the graph
change(cell, opt)Any cell attribute changes
change:*(cell, newValue, opt)A specific attribute changes (e.g. change:position, change:size); newValue is the new attribute value
reset(collection, opt)All cells replaced (e.g. via resetCells())

For the full list of granular change:* and batch:* events, see the Graph API events reference.

Reacting to drag state

Inside renderElement, useCellDrag() reports whether the element being rendered is currently being dragged, so its React content can restyle itself mid-drag without wiring pointer events by hand. It returns a reactive snapshot of the drag:

  • isDragging: whether this element is currently being dragged (the field you'll reach for most).
  • dropArea: the element's current bounding box, as a g.Rect.
  • event: the originating pointer event.
  • paper / graph: the active instances.

Only the dragged element re-renders.

import { useCellDrag } from '@joint/react';

function Card({ label }: CardData) {
const { isDragging } = useCellDrag();
return (
<div className={isDragging ? 'card card--dragging' : 'card'}>
{label}
</div>
);
}

Practical patterns

The event hooks are the main bridge between user interactions and native JointJS APIs. Two common patterns that build on them:

  • Adding Built-in Tools: attach element or link tools (remove buttons, vertex handles, boundary outlines) on hover with the <Paper> onElementMouseEnter / onLinkMouseEnter props
  • Outlining Elements: add highlighter outlines that follow custom SVG geometry, toggled on the <Paper> hover-event props with highlighters.mask

Both guides show full working examples wiring the interaction through <Paper> event props.