Skip to content
D
Documentation

React quick start

tutorial
3 min readUpdated

Build a working diagram in React, connect sibling UI to its live instance, and render kit diagrams and comment threads.

One headless model drives every framework binding: the React components are framework-specific skins over the same diagram engine.

Prerequisites

Use a browser-based React app with React and React DOM 17, 18, or 19. The @grafloria/react 0.10.6 package declares peers @grafloria/engine ^0.3.0, @grafloria/renderer ^0.4.16, and @grafloria/element ^0.4.3.

Install the binding and its peers:

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

1. Mount a flow

Use GrafloriaFlow for an interactive node-and-edge canvas. The starter data uses NodeSpec and EdgeSpec; defaultNodes and defaultEdges seed an uncontrolled diagram, which the instance owns after mount.

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

const nodes: NodeSpec[] = [
  { id: 'ingest', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Ingest' } },
  { id: 'publish', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, data: { label: 'Publish' } },
];

const edges: EdgeSpec[] = [{ id: 'e1', source: 'ingest', target: 'publish' }];

export default function App() {
  return (
    <div style={{ height: '100vh' }}>
      <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} plugins />
    </div>
  );
}
The Ingest and Publish nodes appear connected in the mounted canvas.

The mounted canvas shows Ingest connected to Publish, with the minimap, zoom and fit controls, and background supplied by plugins. Give the wrapper a resolved height; the canvas fills its parent.

2. Keep controlled data in React state

When application UI owns the graph, useNodesState and useEdgesState provide state plus a separate callback for changes coming back from the canvas. Keep both callback connections: without onNodesChange, for example, a node drag can be overwritten by stale controlled state on the next render.

tsx
import { GrafloriaFlow, useEdgesState, useNodesState } from '@grafloria/react';
import type { EdgeSpec, NodeSpec } from '@grafloria/renderer';

const initialNodes: NodeSpec[] = [
  { id: 'ingest', position: { x: 60, y: 80 }, size: { width: 180, height: 80 }, label: 'Ingest' },
  { id: 'publish', position: { x: 380, y: 80 }, size: { width: 180, height: 80 }, label: 'Publish' },
];

const initialEdges: EdgeSpec[] = [{ id: 'e1', source: 'ingest', target: 'publish' }];

export default function ControlledFlow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, , onEdgesChange] = useEdgesState(initialEdges);

  return (
    <div style={{ height: 440, display: 'flex', flexDirection: 'column' }}>
      <button
        onClick={() => setNodes((all) => [
          ...all,
          { id: `step-${all.length}`, position: { x: 220, y: 240 }, label: 'New step' },
        ])}
      >
        Add node
      </button>
      <GrafloriaFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        style={{ flex: 1, minHeight: 0 }}
      />
    </div>
  );
}
The controlled canvas shows its Add node button and the connected Ingest and Publish nodes.

The button adds a node to React state, and dragging or deleting nodes and edges sends the resulting models back through the third tuple values. The second tuple value updates React's specs; it is not the canvas change callback.

3. Reach the canvas from sibling UI

Wrap a toolbar and canvas in GrafloriaProvider when the toolbar is outside the flow subtree. useGrafloria returns the live DiagramInstance, or null until the flow mounts. The toolbar below calls fitView() on that instance.

For live readouts, useSelection returns the current selection as state, useOnSelectionChange runs a callback when it changes, and useViewport returns the camera state. useGrafloriaStore exposes the nearest store itself; use it when you need the store rather than only its instance.

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

const nodes: NodeSpec[] = [
  { id: 'draft', position: { x: 40, y: 80 }, label: 'Draft' },
  { id: 'review', position: { x: 280, y: 80 }, label: 'Review' },
];

const edges: EdgeSpec[] = [{ id: 'e1', source: 'draft', target: 'review' }];

function Toolbar() {
  const instance = useGrafloria();

  return (
    <button disabled={!instance} onClick={() => instance?.fitView()}>
      Fit diagram
    </button>
  );
}

function CanvasReadout() {
  const store = useGrafloriaStore();
  const instance = useGrafloria();
  const { nodes: selectedNodes } = useSelection();
  const { zoom } = useViewport();
  const [selectionEvents, setSelectionEvents] = useState(0);

  useOnSelectionChange(() => setSelectionEvents((count) => count + 1));

  const storeConnected = instance !== null && store?.get() === instance;

  return (
    <output>
      {storeConnected ? 'Canvas connected' : 'Waiting for canvas'} ·{' '}
      {selectedNodes.length} selected · {Math.round(zoom * 100)}% zoom ·{' '}
      {selectionEvents} selection changes
    </output>
  );
}

export default function App() {
  return (
    <GrafloriaProvider>
      <div style={{ height: 440, display: 'flex', flexDirection: 'column' }}>
        <Toolbar />
        <CanvasReadout />
        <GrafloriaFlow
          defaultNodes={nodes}
          defaultEdges={edges}
          style={{ flex: 1, minHeight: 0 }}
        />
      </div>
    </GrafloriaProvider>
  );
}
Fit diagram and the selection and zoom readout sit above the connected canvas.

The toolbar is a sibling of the canvas, and Fit diagram frames its nodes when clicked. The readout shows the selected-node count and zoom; selecting a node updates the count and selection-change total. A single flow can also publish its instance to its own children without a provider; use the provider for components outside that subtree.

4. Render a kit diagram

Use GrafloriaDiagram when you have a diagram spec rather than node and edge props. This example passes a JSON-encoded spec string; the mounted component renders a connected Ingest-to-Publish diagram.

tsx
import { GrafloriaDiagram } from '@grafloria/react';

const spec = JSON.stringify({
  nodes: [
    { id: 'ingest', position: { x: 40, y: 80 }, data: { label: 'Ingest' } },
    { id: 'publish', position: { x: 280, y: 80 }, data: { label: 'Publish' } },
  ],
  edges: [{ id: 'e1', source: 'ingest', target: 'publish' }],
});

export default function KitDiagram() {
  return (
    <div style={{ height: 440 }}>
      <GrafloriaDiagram spec={spec} />
    </div>
  );
}
The diagram shows Ingest connected to Publish.

5. Add a comment panel

Set comments on the flow to enable its comment store, then pass that store to GrafloriaCommentPanel. CommentStore is available from the instance after mount; this example anchors a starter thread to the Review node.

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 nodes: NodeSpec[] = [
  { id: 'design', position: { x: 40, y: 100 }, size: { width: 150, height: 66 }, label: 'Design' },
  { id: 'review', position: { x: 260, y: 100 }, size: { width: 150, height: 66 }, label: 'Review' },
];

const edges: EdgeSpec[] = [{ id: 'e1', source: 'design', target: 'review' }];

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

  return (
    <div style={{ display: 'flex', height: 440 }}>
      <GrafloriaFlow
        defaultNodes={nodes}
        defaultEdges={edges}
        comments
        style={{ flex: 1, minWidth: 0 }}
        onInit={(instance) => {
          const commentStore = instance.getCommentStore();
          if (commentStore) {
            commentStore.createThread(
              { kind: 'node', id: 'review' },
              'Can we tighten the review step?'
            );
            setStore(commentStore);
          }
        }}
      />
      {store && (
        <div style={{ width: 300, overflow: 'auto' }}>
          <GrafloriaCommentPanel store={store} />
        </div>
      )}
    </div>
  );
}
A comment thread about the Review step appears beside the connected Design and Review nodes.

The canvas renders Design and Review with their edge, and the panel appears beside it with a thread attached to Review. The panel follows the store passed at mount; this example keeps that store in React state until the component unmounts.

What you have

You now have an interactive React canvas, controlled state that stays synchronized with edits, sibling UI that reaches the canvas instance, a generic kit-diagram host, and a comment panel bound to the canvas store.

Where next

Was this page helpful?