Interactivity & Events
Your diagram is already interactive: elements drag out of the box. This chapter is about responding to what the user does (clicks, hovers) and wiring those into React state.
- JointJS
- JointJS+
import { useState } from 'react'; import { GraphProvider, Paper, HTMLHost, useCell, selectElementData, linkRoutingOrthogonal, type ElementPort, } from '@joint/react'; import '@joint/react/styles.css'; import './example.css'; type NodeData = { label: string; description: string; icon: string; color: string }; const ports: Record<string, ElementPort> = { in: { cx: 0, cy: '50%', width: 10, height: 10, passive: true }, out: { cx: '100%', cy: '50%', width: 10, height: 10 }, }; const node = (id: string, x: number, y: number, data: NodeData) => ({ id, type: 'element' as const, position: { x, y }, size: { width: 160, height: 52 }, portMap: ports, data, }); const initialCells = [ node('a', 60, 50, { label: 'New Ticket', description: 'Trigger', icon: '⚡', color: '#f59e0b' }), node('b', 320, 50, { label: 'Is Urgent?', description: 'Condition', icon: '◆', color: '#a855f7' }), node('c', 320, 180, { label: 'Send Reply', description: 'Action', icon: '▶', color: '#22c55e' }), { id: 'a→b', type: 'link', source: { id: 'a', port: 'out' }, target: { id: 'b', port: 'in' }, style: { color: '#94a3b8', width: 2 } }, { id: 'b→c', type: 'link', source: { id: 'b', port: 'out' }, target: { id: 'c', port: 'in' }, style: { color: '#94a3b8', width: 2 } }, ]; function NodeCard({ label, description, icon, color }: NodeData) { return ( <HTMLHost useModelGeometry className="node"> <span className="node__icon" style={{ color }}> {icon} </span> <div> <div className="node__label">{label}</div> <div className="node__desc">{description}</div> </div> </HTMLHost> ); } // The event carries only the element's id; read that element's data reactively. function HoveredInfo({ id }: { id: string | number }) { const { label, description } = useCell(id, selectElementData<NodeData>); return <>Hovering <code>{label}</code> · {description}</>; } function HoverHint({ hoveredId }: { hoveredId: string | number | null }) { return ( <span> {hoveredId != null ? ( <HoveredInfo id={hoveredId} /> ) : ( 'Hover a node to inspect it · drag any node to move it' )} </span> ); } export default function App() { const [hoveredId, setHoveredId] = useState<string | number | null>(null); const [interactive, setInteractive] = useState(true); return ( <GraphProvider initialCells={initialCells}> <div className="stage"> <Paper renderElement={NodeCard} linkRouting={linkRoutingOrthogonal()} interactive={interactive} onElementMouseEnter={({ id }) => setHoveredId(id)} onElementMouseLeave={() => setHoveredId(null)} className="canvas" /> <footer className="footer"> <button className="footer__toggle" onClick={() => setInteractive((v) => !v)}> {interactive ? '✎ Editing' : '👁 View only'} </button> <HoverHint hoveredId={hoveredId} /> </footer> </div> </GraphProvider> ); }
Hover an element and the footer below the canvas shows its details; move the pointer away and it clears. Drag any element to move it, and use the Editing / View only button to toggle whether it can be dragged at all.
Dragging and the interactive prop
Moving elements is on by default. The interactive prop on Paper turns it on and off. It's an ordinary prop, so you can drive it from state, which is exactly what the Editing / View only button in the demo does:
const [interactive, setInteractive] = useState(true);
<Paper interactive={interactive} /> // false → read-only, nothing drags
interactive also takes a per-interaction object ({ elementMove: false }) or a function that decides per element, so you can freeze some elements and leave others draggable. For the full read-only-vs-edit pattern, see Interactivity.
Reacting to events
Paper exposes events as props, named on + target + action: onElementMouseEnter, onElementMouseLeave, onElementPointerClick, onBlankPointerClick, onLinkPointerClick, and so on. Each handler receives one context object:
<Paper
onElementMouseEnter={({ id }) => setHoveredId(id)}
onElementMouseLeave={() => setHoveredId(null)}
/>
Targets are Element, Link, and Blank (empty canvas), each with pointer, mouse, and context-menu variants. Pointer and click payloads carry { id, model, paper, graph, event, x, y }; hover variants (onElementMouseEnter / Leave) carry { id, model, paper, graph, event } without x / y; blank payloads drop id and model.
useCallback neededPaper subscribes once and always calls your latest handler, so inline arrows are fine and cause no re-subscription. For raw JointJS event names ('render:done') or events without an on* prop, use the useOnPaperEvents hook.
Wiring events into React state
The pattern is simple: an event updates React state, and your UI reads that state. Keep just the hovered element's id in a useState, feed it from the Paper events, and read that element's data reactively with useCell(id, selectElementData):
const [hoveredId, setHoveredId] = useState(null);
<Paper
onElementMouseEnter={({ id }) => setHoveredId(id)}
onElementMouseLeave={() => setHoveredId(null)}
/>
// Read the hovered element's data by id — reactive, re-renders when it changes
function HoveredInfo({ id }) {
const { label } = useCell(id, selectElementData);
return <>{label}</>;
}
Store the id, not the data: it stays a stable handle, and useCell(id, …) always reads the element's current data and re-renders when it changes. State lives in React, so you can drive any UI from it: a footer, a details panel, a toolbar. Dragging still never re-runs your renderer (position lives in the view layer), so this stays cheap.
Click-to-select, highlighting the selected element, and multi-selection are covered next in Selecting Elements, where JointJS+ provides a built-in selection you don't have to wire by hand.
You have the fundamentals
You can build, style, connect, and interact with a real diagram, entirely with React components and data, no extra dependencies. That is the community core of @joint/react.
Next, add a scrollable, zoomable canvas with JointJS+.