Skip to content
D
Documentation

Instance and lifecycle

concept
3 min readUpdated

A DiagramInstance is the live, renderer-level facade for one mounted diagram: it lets you update specs and pixels without rebuilding the editor.

Framework bindings are thin skins over one headless model. Specs describe your intent, live models hold the data, and the engine owns behavior. Keep the instance for the lifetime of its mounted host; update that instance, then dispose it when the host unmounts.

One instance, three responsibilities

Instance access stays tied to the mounted diagram, not disconnected copies; see the JavaScript quick start for how to access its model and engine.

mermaid
flowchart TD
  H["Mounted host or framework binding"] --> I["DiagramInstance"]
  S["New specs"] --> R["Reconcile by id"]
  R --> M["Live DiagramModel"]
  I -->|"getModel()"| M
  I -->|"getEngine()"| E["DiagramEngine: behavior"]
  E --> M
  M --> P["Queued repaint"]
  I -->|"renderNow()"| F["Synchronous repaint"]
  H -->|"Unmount"| D["dispose()"]

render returns the instance in a browser application. React exposes it through onInit, and Vue through @init. Those are entry points to the same renderer-level surface, not separate diagram implementations.

In React, the binding mounts the instance in an effect and keeps callback props in a ref. New inline callbacks do not recreate it. For controlled and uncontrolled inputs, follow the Qwik quick start; State and event flow covers the change-event return path.

Reconciliation changes data, not the mount

Pass a complete next node list to setNodes() and a complete next edge list to setEdges(). With plain specs, existing ids update their live objects, new ids create objects, and missing ids remove objects. This preserves object identity rather than remounting the diagram. Omit selected to leave the user's selection alone.

The input types are NodeInput and EdgeSpec. Nodes can also be live models: a different live model supplied under an existing id replaces that model. Attached links survive only when the replacement has the same port id or a port on the same side; otherwise they are removed. Plain-spec reconciliation and live-model replacement are different paths; see Specs and live models.

For externally edited imports with reused ids, follow the import guidance in the Vue quick start.

See updates on a mounted instance

This browser sample renders two connected nodes. Reconcile changes their labels and positions through specs. Move together moves both live models down in one batch. Close disposes the diagram and removes its host.

Install the packages the sample imports in your own browser TypeScript project:

bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
ts
import { render } from '@grafloria/element';
import type { DiagramInstance, EdgeSpec, NodeInput } from '@grafloria/renderer';

function mountEditor(parent: HTMLElement): () => void {
  const panel = document.createElement('section');
  const controls = document.createElement('div');
  const host = document.createElement('div');
  host.style.height = '400px';
  panel.append(controls, host);
  parent.append(panel);

  const nodes = [
    { id: 'draft', label: 'Draft', position: { x: 80, y: 80 },
      size: { width: 140, height: 60 } },
    { id: 'review', label: 'Review', position: { x: 320, y: 80 },
      size: { width: 140, height: 60 } },
  ] satisfies NodeInput[];
  const edges: EdgeSpec[] = [
    { id: 'workflow', source: 'draft', target: 'review' },
  ];
  const instance: DiagramInstance = render({ nodes, edges }, host);

  const reconcile = document.createElement('button');
  reconcile.textContent = 'Reconcile';
  reconcile.onclick = () => {
    const nextNodes: NodeInput[] = [
      { id: 'draft', label: 'Draft updated', position: { x: 80, y: 140 },
        size: { width: 140, height: 60 } },
      { id: 'review', label: 'Review updated', position: { x: 320, y: 140 },
        size: { width: 140, height: 60 } },
    ];
    instance.setNodes(nextNodes);
    instance.setEdges(edges);
  };

  const move = document.createElement('button');
  move.textContent = 'Move together';
  move.onclick = () => {
    instance.batchUpdate((model) => {
      for (const node of model.getNodes()) {
        node.setPosition(node.position.x, node.position.y + 30);
      }
    });
  };

  const close = document.createElement('button');
  close.textContent = 'Close';
  const unmount = () => {
    reconcile.onclick = null;
    move.onclick = null;
    close.onclick = null;
    instance.dispose();
    panel.remove();
  };
  close.onclick = unmount;
  controls.append(reconcile, move, close);
  return unmount;
}

mountEditor(document.body);

The initial canvas shows Draft connected to Review from left to right, beneath the Reconcile, Move together, and Close controls.

The reconciliation calls schedule painting themselves. The batch follows the library's own example: mutate the models supplied to the callback rather than building an unrelated model. These are direct model updates; use commands for user-facing edits that need history, as described in Commands and history.

For an interactive example of instance access and engine behavior, open the live drag-and-undo demo.

Queued painting versus synchronous painting

Model mutation and painting have different timing. A setter updates the live data before the queued frame draws it. Choose the painting method according to what your next statement needs:

MethodReturnsWhat you get
render()voidA queued repaint; repeated requests before the frame runs coalesce.
renderNow()voidA synchronous repaint that cancels the pending frame and bypasses idle skipping. Use it before measuring changed diagram DOM.
batchUpdate(mutate)voidImmediate mutations inside the callback, batched model events, and a queued repaint. It does not paint synchronously.

Keep the batch callback synchronous. If you need to measure after a batch, call renderNow() after batchUpdate() returns. Nested batches remain batched, and a throwing callback does not leave the model stuck in batch mode.

Dispose at unmount

In a plain browser host, call dispose() from the host's unmount or close handler, as above. Disposal detaches interaction handlers, cancels scheduled painting, disconnects the resize observer, removes listeners and custom-node hosts, and removes the diagram's DOM. Repeated disposal is a no-op.

The React binding performs disposal in its effect cleanup; let the binding own that teardown rather than disposing immediately after onInit. If you supply an external engine, instance disposal leaves that engine alive: its owner is responsible for destroying it.

For a flat cross-layer facade, createDiagramApi wraps an existing instance; it is not another mount. Continue with React: state and subscriptions for framework instance access, Lay out a diagram for engine layout, or Save and restore documents for persistence from the live model.

Was this page helpful?