Rendering React inside native shapes
@joint/react renders React into its own ElementModel cells out of the box. Portals let you do the same for any other cell type, whether that is a built-in shape or your own dia.Element subclass.
The mechanism is the portalSelector prop on <Paper>: return a selector string per cell, and renderElement's output mounts inside the native shape's SVG markup at exactly that node.
Basic usage​
The example below wires a React Card on each end of a pipeline and a native standard.Cylinder in the middle. JointJS still draws the cylinder's body and rim; everything inside it (the title, the live-status dot, the record count) is React rendered through the portal.
Without portalSelector, the cylinder would still render, but only its native body and rim. The React overlay would have nowhere to mount.
How cells declare their own target​
Records that leave type unset use ElementModel, a cell class that sets portalSelector = '__portal__' on itself and ships markup with a matching <g joint-selector="__portal__" />. That is what makes renderElement "just work" for React-style records.
Native shapes such as standard.Rectangle or standard.Cylinder have their own markup with selectors like body, top, and label, but no portalSelector field. Without the paper-level override, there is nowhere for React content to mount.
'element' is the string constant that identifies ElementModel cells. You will use it to branch inside the override: let ElementModel use its own default, and point everything else at a real selector.
Overriding with a portalSelector function​
The function form gives you per-cell control. It receives { model, paper, graph } and returns one of:
- a selector string: look up that node inside the cell's markup,
- an
Element: use that DOM node directly, null: skip React rendering for this cell,undefined(or no return): fall back to the cell's ownportalSelector.
<Paper
renderElement={renderNode}
portalSelector={({ model }) => {
if (model.get('type') !== 'element') return 'root';
// implicit undefined → use the cell's own `portalSelector`
}}
/>
Two things make this pattern the default recommendation:
- Returning
undefinedforElementModelcells preserves the library's default behavior for every React-rendered record. You don't accidentally break the normal path while opting native shapes into React rendering. - Routing everything else to
'root'lands the React content inside the shape's outermost<g>, so it shares the cell's transform, position, and rotation automatically.
Declaring portalSelector on a custom shape​
When you own the cell class (a custom dia.Element subclass, not a built-in shape), you can skip the Paper-prop override entirely by declaring portalSelector as a field on the class itself. That's the PortalHostCell interface, and it's the same mechanism ElementModel uses for its '__portal__' selector.
import { dia } from '@joint/core';
import type { PortalHostCell } from '@joint/react';
class WidgetShape extends dia.Element implements PortalHostCell {
portalSelector = 'widget'; // any selector that exists in the markup
markup = [
{ tagName: 'rect', selector: 'body' },
{ tagName: 'g', selector: 'widget' }, // React content mounts here
];
}
Any Paper that hosts an instance of WidgetShape will portal renderElement output into its 'widget' node automatically, without the portalSelector prop. Reach for the Paper prop only when you can't modify the class; prefer the class-field form when you can, so each shape carries its own contract.
Branching renderElement per shape​
Once React-rendered and native shapes share a Paper, renderElement still runs for each cell. The demo uses a kind field on data to pick the right component:
function renderNode(data: NodeData) {
if (data.kind === 'overlay') {
return <DatabaseOverlay {...data} />;
}
return <TaskCard {...data} />;
}
TaskCard renders a full-sized React node via HTMLHost the same way the React-first path does. The input and output cells deliberately leave size unset so HTMLHost can measure the Card and size the cell to its content.
The overlay is different. The cylinder already has a declared size, and we want the React content to lay out inside that box rather than compete with it. DatabaseOverlay skips HTMLHost and renders a raw <foreignObject> sized from useCell(selectElementSize):
function DatabaseOverlay({ label, description, stats }: DatabaseOverlayProps) {
const { width, height } = useCell(selectElementSize);
return (
<foreignObject width={width} height={height}>
{/* HTML content that lays out inside the cylinder's bounding box */}
</foreignObject>
);
}
foreignObject is a plain SVG primitive that embeds HTML, so there is no content-measurement loop: the cell's size stays the source of truth.
If the selector returned by portalSelector does not resolve in a given cell's markup, React rendering for that cell is silently skipped, with no error and no log. The function form is the safe default because returning undefined lets each cell use its own portalSelector field, and you can scope explicit returns to the shapes you know expose your target.
Content portaled into 'root' is appended as the last child of that <g>, so it paints above the shape's own body, top, and label. If the native shape comes with default decorations you don't want, clear them through attrs (for example attrs.label.text: '') so React owns the content on that cell.
When to use portal selectors​
Reach for portalSelector when:
- you want React content inside any built-in JointJS shape or your own custom
dia.Elementsubclass - you're mixing React-rendered and purely-native elements in one graph and want both to show React content
Stay on the default path (no portalSelector) when your entire graph is React-rendered.