# React hooks and components

React bindings subscribe to one headless diagram instance. Choose controlled hooks when React owns the graph, provider and subscription hooks when UI outside the canvas needs live state, and a higher-level component when you already have a render spec or dashboard data.

## How the parts fit together

The [React quick start](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/react-quick-start) covers the basic `GrafloriaFlow` setup and controlled graph state. This page adds how the hooks attach to the mounted [`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) without creating a second diagram model.

```mermaid
flowchart TB
  R["React state"] -->|nodes and edges| F["GrafloriaFlow"]
  F --> I["DiagramInstance"]
  I -->|nodes:change| N["useNodesState onNodesChange"]
  I -->|selection:change| S["useSelection or useOnSelectionChange"]
  I -->|viewport:change| V["useViewport"]
  N --> R
```

`GrafloriaFlow` needs a parent with a real height. The example below produces two connected nodes, keeps their positions in React state after a drag, and shows the selected node and camera values in a sibling toolbar.

## Choose the component boundary

- Use `GrafloriaFlow` for a node-and-edge editor; see the [React quick start](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/react-quick-start) for its initial and controlled data forms.
- Use [`GrafloriaDiagram`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#grafloriadiagram) for a separately mounted diagram. The component exposes `spec`, optional `options`, and `onReady`; the mounted result is still a `DiagramInstance`.
- Use [`GrafloriaDashboard`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#grafloriadashboard) for a widget board. Give it `views` for a multi-view board or `widgets` for the single-view shorthand, and use `onReady` for its typed handle.
- Use [`GrafloriaCommentPanel`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#grafloriacommentpanel) beside a flow when comments are enabled. Pass the flow's `CommentStore` to `store`; the panel displays its threads and calls `onSelect` with the selected thread id.

Reach from a flow to its instance with `onInit` when the consumer is in the same component. When a toolbar, inspector, or minimap is a sibling, put both components inside [`GrafloriaProvider`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#grafloriaprovider) and use [`useGrafloria`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#usegrafloria).

## Keep the graph controlled

[`useNodesState`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#usenodesstate) returns `[nodes, setNodes, onNodesChange]`. The first value is `NodeSpec[]`, the setter changes React-owned specs, and the third value accepts the live node models emitted by the flow. [`useEdgesState`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#useedgesstate) has the corresponding edge tuple.

```tsx
import { useRef, useState } from 'react';
import {
  GrafloriaFlow,
  GrafloriaProvider,
  useEdgesState,
  useGrafloria,
  useGrafloriaStore,
  useNodesState,
  useOnSelectionChange,
  useSelection,
  useViewport,
} from '@grafloria/react';
import type { DiagramInstance, EdgeSpec, NodeSpec } from '@grafloria/renderer';

const initialNodes: NodeSpec[] = [
  { id: 'draft', position: { x: 80, y: 120 }, size: { width: 150, height: 66 }, label: 'Draft' },
  { id: 'review', position: { x: 340, y: 120 }, size: { width: 150, height: 66 }, label: 'Review' },
];

const initialEdges: EdgeSpec[] = [
  { id: 'draft-review', source: 'draft', target: 'review', label: 'submit' },
];

function Toolbar() {
  const grafloria = useGrafloria();
  const store = useGrafloriaStore();
  const { nodes } = useSelection();
  const { zoom, x, y } = useViewport();
  const [inspectedId, setInspectedId] = useState<string | null>(null);

  useOnSelectionChange(({ nodes: selectedNodes }) => {
    setInspectedId(selectedNodes[0]?.id ?? null);
  });

  return (
    <div style={{ display: 'flex', gap: 12, padding: 8 }}>
      <button type="button" onClick={() => grafloria?.fitView()}>Fit view</button>
      <button type="button" onClick={() => grafloria?.renderNow()}>Render now</button>
      <span>selected: {inspectedId ?? nodes[0]?.id ?? 'none'}</span>
      <span>camera: {zoom.toFixed(2)} ({x.toFixed(0)}, {y.toFixed(0)})</span>
      <span>store: {store?.get() === grafloria ? 'connected' : 'waiting'}</span>
    </div>
  );
}

export function ControlledFlow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
  const instance = useRef<DiagramInstance | null>(null);

  return (
    <GrafloriaProvider>
      <Toolbar />
      <div style={{ height: 420 }}>
        <GrafloriaFlow
          nodes={nodes}
          onNodesChange={onNodesChange}
          edges={edges}
          onEdgesChange={onEdgesChange}
          onInit={(liveInstance) => { instance.current = liveInstance; }}
          fitView
        />
      </div>
      <button
        type="button"
        onClick={() => setNodes((current) => current.map((node) => (
          node.id === 'review' ? { ...node, label: 'Approved' } : node
        )))}
      >
        Rename review
      </button>
      <button type="button" onClick={() => instance.current?.fitView()}>Fit from onInit</button>
    </GrafloriaProvider>
  );
}
```

The `onNodesChange` and `onEdgesChange` callbacks close the loop: a committed user edit enters the hook, becomes a new spec array, and reaches the controlled props. Omitting either callback leaves React with stale state, so the next render can put the model back at its old position. Keep the arrays in hook state rather than creating a fresh literal in every render.

## Reach state from nearby UI

The [Vue quick start](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/vue-quick-start) explains the corresponding selection and viewport hook roles. In React, this page adds the placement rule: put these subscriptions in toolbar or inspector components that live inside the provider or flow subtree.

`useGrafloria` returns `null` until the flow mounts. The toolbar above therefore uses optional chaining. The hook works in any descendant of `GrafloriaProvider`; a flow also publishes its instance to its own store, so children passed through `GrafloriaFlow` do not need a provider. [`useGrafloriaStore`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-react#usegrafloriastore) is the lower-level store access and is useful only when implementing a binding-level integration rather than ordinary application UI.

For a same-component consumer, `onInit` gives you the instance directly. Call instance methods such as `fitView()`, `renderNow()`, `setNodes()`, `setEdges()`, `export()`, and `dispose()` on the instance. Put `dispose()` in your application's unmount cleanup, not immediately after mounting the flow.

## Use the spec and board components

`GrafloriaDiagram` mounts the serialized JSON document below into a sized parent and invokes `onReady` with the instance:

```tsx
import { GrafloriaDiagram } from '@grafloria/react';
import type { DiagramInstance, EdgeSpec, NodeSpec } from '@grafloria/renderer';

const diagramNodes: NodeSpec[] = [
  { id: 'draft', position: { x: 80, y: 120 }, size: { width: 150, height: 66 }, label: 'Draft' },
  { id: 'review', position: { x: 340, y: 120 }, size: { width: 150, height: 66 }, label: 'Review' },
];

const diagramEdges: EdgeSpec[] = [
  { id: 'draft-review', source: 'draft', target: 'review', label: 'submit' },
];

const diagramSpec = { nodes: diagramNodes, edges: diagramEdges };

export function ReadOnlyDiagram() {
  const onReady = (instance: DiagramInstance) => {
    instance.fitView();
  };

  return (
    <div style={{ height: 360 }}>
      <GrafloriaDiagram
        spec={JSON.stringify(diagramSpec)}
        onReady={onReady}
        style={{ height: '100%' }}
      />
    </div>
  );
}
```

Use `GrafloriaDashboard` when the data is a widget board rather than a node graph. `layout`, `sizing`, and `static` are component props; `onReady` gives the live dashboard handle.

When the flow has comments enabled, obtain its store from the instance and pass it to `GrafloriaCommentPanel`. The panel then renders the thread beside the canvas:

```tsx
import { useState } from 'react';
import { GrafloriaCommentPanel, GrafloriaFlow } from '@grafloria/react';
import type { CommentStore } from '@grafloria/engine';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const commentNodes: NodeSpec[] = [
  { id: 'review', position: { x: 100, y: 120 }, size: { width: 160, height: 70 }, label: 'Review' },
];

const commentEdges: EdgeSpec[] = [];

export function CommentedFlow() {
  const [store, setStore] = useState<CommentStore | null>(null);

  return (
    <div style={{ display: 'flex', height: 320 }}>
      <GrafloriaFlow
        defaultNodes={commentNodes}
        defaultEdges={commentEdges}
        comments
        style={{ flex: 1 }}
        onInit={(instance) => {
          const commentStore = instance.getCommentStore();
          if (commentStore) {
            const threadId = commentStore.createThread(
              { kind: 'node', id: 'review' },
              'Please review this step.',
            );
            commentStore.reply(threadId, 'The step is ready.');
            setStore(commentStore);
          }
        }}
      />
      {store && <GrafloriaCommentPanel store={store} />}
    </div>
  );
}
```

The result is a flow with an anchored conversation panel; `getCommentStore()` is `null` when comments are not enabled.

```tsx
import { GrafloriaDashboard } from '@grafloria/react';
import type { DashboardViewSpec } from '@grafloria/element';

const views: DashboardViewSpec[] = [{
  id: 'main',
  widgets: [
    { id: 'revenue', kind: 'kpi', span: 4, rows: 1, data: { label: 'Revenue', value: '$6.81M' } },
    { id: 'customers', kind: 'kpi', span: 4, rows: 1, data: { label: 'Customers', value: '1,284' } },
  ],
}];

export function MetricsBoard() {
  return (
    <div style={{ height: 360 }}>
      <GrafloriaDashboard views={views} layout="grid" sizing="fit" />
    </div>
  );
}
```

## What to remember

- The [React quick start](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/react-quick-start) covers controlled versus instance-owned graph data; this page adds the choice between a flow, a standalone diagram, a dashboard, and a comment panel based on the data each component consumes.
- A flow fills its parent, so give the canvas a height.
- The instance is the facade for rendering, events, viewport operations, and export. Use the model or engine only when the instance API does not cover the operation.
- A provider is needed for sibling consumers, not for a single canvas or its children.
- `useSelection` renders from current state; `useOnSelectionChange` runs a callback; `useViewport` renders camera state.

See the [React quick start](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/react-quick-start), [customize nodes](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/customize-nodes), [build a dashboard](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/build-a-dashboard), and [the DiagramInstance reference](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance).

[Open the React demo gallery](https://grafloria.com/demos-react/) to run the same binding against live diagrams.
