# React: custom content

Use React components when a node or dashboard widget needs application UI, context or local state. You paint the inside of the box; the engine owns its geometry and gestures. For a different silhouette or fill without application UI, use the shipped shapes instead; see [JavaScript: elements and content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-elements-and-content).

The examples below run in your React application's browser entry. Install the binding and its peers:

```bash
npm install @grafloria/react @grafloria/engine @grafloria/renderer @grafloria/element react react-dom
```

## 1. Render components inside connected nodes

[`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriaflow) maps `nodeTypes` keys to node `type` strings. Your component receives [`NodeProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#nodeprops):

- `id`: the node's id.
- `data`: the node's payload, not a separate set of component props.
- `selected`: the live selection state.
- `node`: the live [`NodeModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-nodemodel#nodemodel), for queries or tracked setters beyond the other props.

Type the registry with [`NodeTypes`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#nodetypes), the nodes with [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec#nodespec), and the connections with [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec). Each custom spec below carries `custom: true`; for the HTML rendering opt-in, see [elements and content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-elements-and-content).

Replace your application's `App.tsx` with this example. It renders Build and Deploy cards connected by an edge. Each card reads the surrounding React context; the checkbox changes the owner line in both cards.

```tsx title="App.tsx"
import { createContext, useContext, useState } from 'react';
import {
  GrafloriaFlow,
  type NodeProps,
  type NodeTypes,
} from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

type CardData = { title: string; owner: string };
const ShowOwners = createContext(true);

function Card({ id, data, selected }: NodeProps<CardData>) {
  const showOwners = useContext(ShowOwners);
  return (
    <div
      aria-label={`${data.title} (${id})`}
      style={{
        width: '100%',
        height: '100%',
        boxSizing: 'border-box',
        padding: '12px 16px',
        background: '#fff',
        color: '#232a3d',
        borderRadius: 12,
        border: selected ? '2px solid #3b52d9' : '2px solid #94a5f0',
        font: '14px/1.5 system-ui, sans-serif',
      }}
    >
      <strong>{data.title}</strong>
      {showOwners && <div>Owner: {data.owner}</div>}
    </div>
  );
}

const nodeTypes: NodeTypes = { card: Card };
const nodes: NodeSpec[] = [
  {
    id: 'build', type: 'card', custom: true,
    position: { x: 80, y: 90 }, size: { width: 230, height: 110 },
    data: { title: 'Build', owner: 'CI' },
  },
  {
    id: 'deploy', type: 'card', custom: true,
    position: { x: 430, y: 90 }, size: { width: 230, height: 110 },
    data: { title: 'Deploy', owner: 'CD' },
  },
];
const edges: EdgeSpec[] = [
  { id: 'build-deploy', source: 'build', target: 'deploy' },
];

export default function App() {
  const [showOwners, setShowOwners] = useState(true);
  return (
    <ShowOwners.Provider value={showOwners}>
      <label>
        <input
          type="checkbox"
          checked={showOwners}
          onChange={(event) => setShowOwners(event.target.checked)}
        />
        Show owners
      </label>
      <div style={{ height: 400 }}>
        <GrafloriaFlow
          defaultNodes={nodes}
          defaultEdges={edges}
          nodeTypes={nodeTypes}
          fitView
        />
      </div>
    </ShowOwners.Provider>
  );
}
```

![Build and Deploy cards connected by an arrow, with owner lines and the Show owners checkbox.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/1cf8a739328bad8a23f1dce58880db27.png)

Click a card to change its selection border, or drag it to move its box. The binding uses React portals, so context and hooks remain part of your application tree rather than a separate React root. Removing a node drops its portal and unmounts the component.

Give each node a `size` and fill that box with `width: '100%'`, `height: '100%'` and `boxSizing: 'border-box'`. Padding and borders then stay inside the geometry used for hit-testing and connections. Declare ports on the spec, not as elements inside the card; see [port connections](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/validate-port-connections).

### When node content refreshes

The binding refreshes custom components on `nodes:change` and `selection:change`, reading `node.isSelected()` again. It also observes the node's data and metadata writes and supplies a fresh `data` object. Use `node.setData()` rather than assigning payload properties directly.

Position changes during a drag move the host rather than triggering a content render on every frame. Keep positioning out of your component's styles. Your own React state and context still render normally.

These examples use uncontrolled defaults. For controlled data and its change-event return path, follow the [React quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-quick-start).

## 2. Mix custom widgets with shipped painters

[`GrafloriaDashboard`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriadashboard) uses `widgetTypes` to map a widget's `kind` to a component. [`WidgetProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#widgetprops) supplies `{ widget, data }`: the full widget spec and its payload. Unlike nodes, dashboard widgets need no `custom` flag.

Use the shipped `kpi`, `line`, `bar`, `donut`, `funnel` and `table` painters for those kinds. Register a component only where you need your own UI; unmatched kinds pass to the built-in renderer.

> **Known issue:** A painter supplied through `options.renderWidget` never runs in the React binding: the wrapper overwrites that option with its portal callback. Until this is fixed, supply custom content through `widgetTypes` and keep `options` for board behavior and geometry.

The kit's intended custom-painting option is `options.renderWidget`; the React equivalent is `widgetTypes={{ note: Note }}`, used below. Type that mapping with [`WidgetTypes`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#widgettypes) and the data with [`DashboardWidgetSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-dashboardwidgetspec#dashboardwidgetspec).

Replace `App.tsx` with this independent example. It renders a shipped Revenue KPI beside a React release note. The note reads application context and keeps an acknowledgement count in local state. The toolbar switches the mounted board between fit and grow sizing.

```tsx title="App.tsx"
import { createContext, useContext, useState } from 'react';
import {
  GrafloriaDashboard,
  type WidgetProps,
  type WidgetTypes,
} from '@grafloria/react';
import type { DashboardWidgetSpec } from '@grafloria/element';

const Team = createContext('Release team');

function Note({ widget, data }: WidgetProps) {
  const team = useContext(Team);
  const [acknowledgements, setAcknowledgements] = useState(0);
  const text = typeof data.text === 'string' ? data.text : '';
  return (
    <section style={{
      width: '100%', height: '100%', boxSizing: 'border-box',
      padding: 16, background: '#eef2ff', color: '#232a3d',
      borderRadius: 10, font: '14px/1.5 system-ui, sans-serif',
    }}>
      <strong>{widget.title}</strong>
      <div>{team}</div>
      <p>{text}</p>
      <button onClick={() => setAcknowledgements((count) => count + 1)}>
        Acknowledge ({acknowledgements})
      </button>
    </section>
  );
}

const widgetTypes: WidgetTypes = { note: Note };
const widgets: DashboardWidgetSpec[] = [
  {
    id: 'revenue', kind: 'kpi', span: 3, rows: 1,
    data: { label: 'Revenue', value: '$6.81M', delta: 12.4 },
  },
  {
    id: 'release', kind: 'note', span: 3, rows: 1,
    title: 'Release note', data: { text: 'Deployment approved for Friday.' },
  },
];

export default function App() {
  const [sizing, setSizing] = useState<'fit' | 'grow'>('fit');
  return (
    <Team.Provider value="Release team">
      <button onClick={() => setSizing('fit')}>Fit rows</button>
      <button onClick={() => setSizing('grow')}>Grow rows</button>
      <span> Sizing: {sizing}</span>
      <div style={{ height: 400 }}>
        <GrafloriaDashboard
          widgets={widgets}
          widgetTypes={widgetTypes}
          options={{ columns: 6, gap: 8, rowHeight: 180 }}
          sizing={sizing}
        />
      </div>
    </Team.Provider>
  );
}
```

![Revenue KPI beside the release note, with Acknowledge (0), Fit rows and Grow rows controls and Sizing: fit.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/2a0dadac8f136a75c3cb7c94233570f6.png)

Custom widgets also mount through portals. Switching `sizing` calls the board handle rather than remounting the board, so the note's acknowledgement state remains intact.

## 3. Mount a spec without React content registrations

Use [`GrafloriaDiagram`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriadiagram) when you already have a [`RenderSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#renderspec) rather than React node or widget components. It accepts plain diagram specs as well as kit specs. This independent `App.tsx` renders two connected, shipped silhouettes without a `nodeTypes` registry.

```tsx title="App.tsx"
import { GrafloriaDiagram } from '@grafloria/react';
import type { RenderSpec } from '@grafloria/element';

const spec: RenderSpec = {
  nodes: [
    {
      id: 'ingest', position: { x: 60, y: 80 },
      size: { width: 180, height: 80 }, label: 'Ingest',
      shape: { type: 'terminal', fill: '#ecfdf5', stroke: '#059669' },
    },
    {
      id: 'publish', position: { x: 380, y: 80 },
      size: { width: 180, height: 80 }, label: 'Publish',
      shape: { type: 'document', fill: '#fdf4ff', stroke: '#9333ea' },
    },
  ],
  edges: [{ id: 'ingest-publish', source: 'ingest', target: 'publish' }],
};

export default function App() {
  return (
    <div style={{ height: 400 }}>
      <GrafloriaDiagram spec={spec} options={{ fitView: true }} />
    </div>
  );
}
```

Unlike the mount-once dashboard inputs, a changed `spec` or `options` value replaces this component's diagram. An equal value rebuilt on a React render keeps the existing instance. For React components inside boxes, keep using `GrafloriaFlow` or `GrafloriaDashboard` above.

## Options that matter

Board geometry lives in [`DashboardOptions`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-dashboardoptions#dashboardoptions). The following defaults come from the kit; the example explicitly selects fit sizing at first render.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `columns` | `number` | `12` | Sets the column count for views without an override. |
| `gap` | `number` | `8` | Sets the widget gap and board padding in pixels. |
| `rowHeight` | `number` | `130` | Sets row height in grow mode. |
| `sizing` | `'fit' \| 'grow'` | Grow for fluid boards; fit for fixed boards | Fit keeps the board height and squeezes rows; grow extends downward at `rowHeight`. |
| `layout` | `'grid' \| 'split'` | `'grid'` | Selects cells or a splitter tree; split always uses fit sizing. |
| `mode` | `'fluid' \| 'fixed'` | Fluid unless `width` is supplied | Fluid uses the container's CSS dimensions; fixed uses authored world dimensions. |

### Respect the mount-once board props

`views`, `widgets` and `options` seed the dashboard at mount. Replacing those props later does not rebuild or reconcile the board. Use either `views` for multiple boards or `widgets` for one board, not both.

The live props are `activeView`, `layout`, `sizing` and `static`. Changes call the handle's `showView()`, `setLayout()`, `setSizing()` and `setStatic()` respectively. Top-level `layout`, `sizing` and `static` override the corresponding `options` fields at mount.

For runtime widget edits, capture the [`DashboardHandle`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-dashboard-kit-dashboardhandle#dashboardhandle) with `onReady` and use its widget methods rather than replacing the initial array. Use `onLayoutChange` to receive the affected view's widgets after committed gestures. See [Build a dashboard](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/build-a-dashboard) for handle-driven editing.

## Demos and related guides

- [Custom React nodes](https://grafloria.com/demos-react/#/nodes/html-nodes): components inside connected nodes.
- [Custom shapes](https://grafloria.com/demos/nodes/custom-nodes.html): use shipped silhouettes when no component is needed.
- [Dashboard builder](https://grafloria.com/demos/dashboard/dashboard-builder.html): a data-first board with shipped widget kinds.
- [React state and subscriptions](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-state-and-subscriptions): keep application state aligned with model events.
- [Theme a canvas](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/theme-a-canvas): size the canvas and integrate its appearance with your application.
