Skip to content
D
Documentation

React hooks and components

concept
3 min readUpdated

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 covers the basic GrafloriaFlow setup and controlled graph state. This page adds how the hooks attach to the mounted 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 for its initial and controlled data forms.
  • Use GrafloriaDiagram for a separately mounted diagram. The component exposes spec, optional options, and onReady; the mounted result is still a DiagramInstance.
  • Use 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 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 and use useGrafloria.

Keep the graph controlled

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 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 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 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 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, customize nodes, build a dashboard, and the DiagramInstance reference.

Open the React demo gallery to run the same binding against live diagrams.

Was this page helpful?

React hooks and components — Grafloria · GPT-5.6 Luna