# State and event flow

State ownership determines whether Grafloria keeps edits in its live model or returns them to your application so controlled inputs stay in sync.

The framework bindings are thin skins over one headless model: specs describe your intent, live models hold the data, and the engine owns behavior. You choose the owner separately for nodes, edges and, where supported, groups.

## Choose the state owner

| Input | Owner | What happens after mounting |
| --- | --- | --- |
| `defaultNodes`, `defaultEdges`, `defaultGroups` | The instance | Defaults seed the collections once. Changing a default later does not reconcile the collection. |
| `nodes`, `edges` | Your application | Updated inputs reconcile into the live model; change callbacks or two-way bindings return canvas edits. |
| `groups` | Your application supplies the group collection | Updated inputs reconcile group membership and frames. There is no corresponding group-change callback in the flow bindings. |

React, Vue and Qwik expose these default and controlled inputs. For an uncontrolled Angular canvas, leave `nodes` and `edges` unbound and pass an engine through `[engine]`. Its canvas has no `groups` or `defaultGroups` input.

Choose defaults for a self-contained canvas. Choose controlled inputs when an inspector or other application UI needs to mirror edits. Controlled specs reconcile into existing models rather than remounting the canvas. Stable node ids keep live identity; omitting `selected` leaves the current selection alone.

```mermaid
flowchart LR
  A["Application specs"] -->|"Controlled inputs"| B["Reconcile live models"]
  D["Defaults at mount"] --> B
  B --> C["Engine and renderer"]
  C -->|"User edits"| B
  B -->|"Change callback or model write"| A
```

Groups are zones with real membership, not a second list of nodes. A [`GroupSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance#groupspec) names members through `children`; without `bounds`, its frame fits those members with padding. You can also pass a live [`GroupModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-groupmodel#groupmodel). Removing a group through `setGroups()` keeps its nodes. For group-edit persistence, read the live document rather than expecting a group-change callback; see [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents).

## Close the return path in your binding

These examples render two connected boxes and a selection count. Drag a box, release it, then select a box or the edge: the application mirrors the node and edge collections and displays the selected counts. React, Vue and Qwik also seed a zone through `defaultGroups`; the zone remains instance-owned.

Use the shared [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec#nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec) vocabulary in each framework. Put this file beside the component you choose:

```ts title="graph.ts"
import type { NodeSpec, EdgeSpec, GroupSpec } from '@grafloria/renderer';

export const initialNodes: NodeSpec[] = [
  { id: 'a', label: 'Input', position: { x: 80, y: 100 },
    size: { width: 140, height: 70 } },
  { id: 'b', label: 'Output', position: { x: 320, y: 100 },
    size: { width: 140, height: 70 } },
];
export const initialEdges: EdgeSpec[] = [
  { id: 'ab', source: 'a', target: 'b' },
];
export const initialGroups: GroupSpec[] = [
  { id: 'pipeline', label: 'Pipeline', children: ['a', 'b'], padding: 40 },
];
```

### React

[`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#grafloriaflow) calls `onNodesChange` with live [`NodeModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-nodemodel#nodemodel) objects and `onEdgesChange` with live [`LinkModel`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-engine-models-linkmodel#linkmodel) objects. [`useNodesState`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#usenodesstate) and [`useEdgesState`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react#useedgesstate) convert those models back to specs. Their third tuple elements close the return path; their second elements are application state setters.

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

```tsx title="Editor.tsx"
import { useState } from 'react';
import { GrafloriaFlow, useNodesState, useEdgesState } from '@grafloria/react';
import { initialNodes, initialEdges, initialGroups } from './graph';

export default function Editor() {
  const [nodes, , onNodesChange] = useNodesState(initialNodes);
  const [edges, , onEdgesChange] = useEdgesState(initialEdges);
  const [selection, setSelection] = useState('0 nodes, 0 edges');

  return (
    <section>
      <p>Selected: {selection}</p>
      <GrafloriaFlow
        nodes={nodes} edges={edges} defaultGroups={initialGroups}
        onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
        onSelectionChange={({ nodes: pickedNodes, edges: pickedEdges }) =>
          setSelection(`${pickedNodes.length} nodes, ${pickedEdges.length} edges`)}
        style={{ height: 400 }}
      />
    </section>
  );
}
```

![Input connects to Output inside the Pipeline zone. The selection count starts at 0 nodes, 0 edges.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/102cf410caecc0fc6f1a1bd50bcf96dc.png)

Keep controlled arrays in state: React's inbound effects depend on their references. For the stale-state pitfall and the full hook contract, see [React quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-quick-start) and [React: state and subscriptions](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-state-and-subscriptions).

### Vue

[`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue#grafloriaflow) emits spec arrays through `update:nodes` and `update:edges`; `v-model` writes them into your refs. The `selection-change` event returns the selected live models, not a spec projection.

```bash
npm install @grafloria/vue @grafloria/engine @grafloria/renderer @grafloria/element vue
```

```vue title="Editor.vue"
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { initialNodes, initialEdges, initialGroups } from './graph';

const nodes = ref<NodeSpec[]>(initialNodes);
const edges = ref<EdgeSpec[]>(initialEdges);
const selection = ref('0 nodes, 0 edges');
</script>

<template>
  <section>
    <p>Selected: {{ selection }}</p>
    <GrafloriaFlow
      v-model:nodes="nodes" v-model:edges="edges"
      :default-groups="initialGroups"
      @selection-change="selection = `${$event.nodes.length} nodes, ${$event.edges.length} edges`"
      style="height: 400px"
    />
  </section>
</template>
```

Vue accepts replacement arrays and in-place changes and skips reapplying its own emitted arrays. See [Vue: state and composables](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/vue-state-and-composables) for the reactive update paths.

### Qwik

[`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow) projects live models to specs before invoking `onNodesChange$` and `onEdgesChange$`. Store those arrays in signals. The selection callback below stores only a string, not its live-model payload.

```bash
npm install @grafloria/qwik @grafloria/engine @grafloria/renderer @grafloria/element @builder.io/qwik
```

```tsx title="Editor.tsx"
import { component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { initialNodes, initialEdges, initialGroups } from './graph';

export default component$(() => {
  const nodes = useSignal<NodeSpec[]>(initialNodes);
  const edges = useSignal<EdgeSpec[]>(initialEdges);
  const selection = useSignal('0 nodes, 0 edges');

  return (
    <section>
      <p>Selected: {selection.value}</p>
      <GrafloriaFlow
        nodes={nodes.value} edges={edges.value} defaultGroups={initialGroups}
        onNodesChange$={(next) => { nodes.value = next; }}
        onEdgesChange$={(next) => { edges.value = next; }}
        onSelectionChange$={(change) => {
          selection.value = `${change.nodes.length} nodes, ${change.edges.length} edges`;
        }}
        style={{ height: '400px' }}
      />
    </section>
  );
});
```

See [Qwik: state and resumption](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-state-and-resumption) for storing and reaching a browser-only instance.

### Angular

[`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent#diagramcanvascomponent) exposes `nodes` and `edges` as two-way model signals. Its input types also accept live models, so use those declared unions for your signals. [`SelectionChange`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-interfaces#selectionchange) contains the selected nodes and edges after the change.

```bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer @grafloria/element rxjs
```

```ts title="editor.component.ts"
import { Component, signal } from '@angular/core';
import { DiagramCanvasComponent, type SelectionChange } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import type { NodeModel, LinkModel } from '@grafloria/engine';
import { initialNodes, initialEdges } from './graph';

@Component({
  selector: 'app-editor',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <p>Selected: {{ selection() }}</p>
    <grafloria-diagram-canvas
      [(nodes)]="nodes" [(edges)]="edges"
      (selectionChange)="onSelection($event)"
      style="display:block; height:400px" />
  `,
})
export class EditorComponent {
  readonly nodes = signal<readonly (NodeSpec | NodeModel)[] | undefined>(initialNodes);
  readonly edges = signal<readonly (EdgeSpec | LinkModel)[] | undefined>(initialEdges);
  readonly selection = signal('0 nodes, 0 edges');

  onSelection(change: SelectionChange): void {
    this.selection.set(`${change.nodes.length} nodes, ${change.edges.length} edges`);
  }
}
```

![Input and Output are connected by an arrow, without a group frame. The selection count reads 0 nodes, 0 edges.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/3c196653c74e554742dbeaea3ccc2f34.png)

Alongside the next-array outputs, `(modelChange)` emits an incremental patch of added, removed and modified entities, including groups. Inbound `nodes` and `edges` writes are not echoed as patches. `[skipModelUpdate]="true"` suspends inbound reconciliation while outbound emissions continue. See [Angular: state and tooling](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/angular-state-and-tooling) for persistence and component methods.

## Learn the complete instance event map

[`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) exposes `on()` for the complete [`DiagramEventMap`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance#diagrameventmap). Use your binding's events first; use the instance when it does not expose the event you need. `on()` returns an unsubscribe function; invoke it when your subscriber unmounts. `off()` removes a handler by identity.

| Event | Payload | What you receive |
| --- | --- | --- |
| `nodes:change` | `{ nodes: NodeModel[] }` | The current node collection, not a per-node delta. |
| `edges:change` | `{ edges: LinkModel[] }` | The current link collection. |
| `selection:change` | `{ nodes: NodeModel[]; edges: LinkModel[] }` | The selected nodes and edges after the change. |
| `connect` | `{ link: LinkModel }` | The added link; the model's link-add handler emits this, including programmatic additions. |
| `reconnect` | `{ link: LinkModel; endpoint: 'source' \| 'target' }` | The link and endpoint after a successful reconnection gesture. |
| `node:click` | `{ node: NodeModel; world: { x: number; y: number } }` | The clicked node and diagram coordinates. |
| `node:doubleclick` | `{ node: NodeModel; world: { x: number; y: number } }` | The double-clicked node and diagram coordinates. |
| `edge:click` | `{ edge: LinkModel; world: { x: number; y: number } }` | The clicked edge and diagram coordinates. |
| `viewport:change` | `{ viewport: Rectangle; zoom: number }` | The camera rectangle and zoom. |
| `ready` | `void` | A one-shot notification queued on a microtask after the initial paint. |

[`Rectangle`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-types-interfaces-b-s#rectangle) is the viewport rectangle type. The flow callbacks also expose initialization, layout completion and collaboration readiness; these are binding hooks, not extra names in `DiagramEventMap`.

React uses `onSelectionChange`, `onConnect`, `onNodeClick` and `onEdgeClick`; Vue uses `@selection-change`, `@connect`, `@node-click` and `@edge-click`; Qwik uses their `$` callback equivalents. Angular exposes `(selectionChange)` but no matching click or connect output on this canvas.

## Changes are not a per-frame state stream

In the shared instance, `node:changed` and `link:changed` schedule repainting without emitting collection-change events. Adds, removals and clears emit collections. The built-in node drag emits `nodes:change` after a moved drag ends; it does not send a new spec array on each pointer move. Selection has its own event and does not require rewriting the document collection.

Treat React, Vue and Qwik state as a mirror that catches up at edit boundaries, not as the animation clock. An arbitrary direct model mutation can repaint without triggering their collection callbacks. For user-facing edits and history, use the command path described in [Commands and history](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history).

Angular has a distinct outbound implementation: it captures model mutations and coalesces a burst into a microtask before emitting `modelChange` and the bound arrays. Do not assume its emissions share the instance's drag-commit timing, or that one array event equals one undo step.

For interaction feedback while drawing a connection, see [Configure editing gestures](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/configure-editing-gestures). Watch the [live connection-event demo](https://grafloria.com/demos/interaction/connection-events.html) for the connection lifecycle rather than treating collection changes as pointer-move events.

## Related

- [Specs and live models](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/specs-and-live-models): the data carried in each direction.
- [Instance and lifecycle](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/instance-and-lifecycle): instance access and subscription cleanup.
- [Group and nest nodes](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/group-and-nest-nodes): membership and frames.
- [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents): persistence beyond framework projections.
