# Qwik: state and resumption

Use QRL callbacks and instance signals to connect a resumable diagram to sibling controls. You get two connected nodes, a Fit view button, selection and camera readouts, and a server-rendered version that becomes interactive in the browser.

The binding is a thin skin over the shared model: keep plain specs in resumable state and use the live instance for rendering operations. Start with an existing Qwik 1.x project using `@builder.io/qwik` ^1.5.0 and its optimizer; see the [Qwik quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-quick-start) for project setup.

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

## 1. Keep the diagram input as data

Share the [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflow) input data—typed with [`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)—between the sibling-subscription and server-adoption examples below; see the [React quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-quick-start) for basic node-and-edge setup.

```ts title="diagram-data.ts"
import type { NodeSpec, EdgeSpec } from '@grafloria/qwik';

export const nodes: NodeSpec[] = [
  { id: 'plan', label: 'Plan', position: { x: 60, y: 80 },
    size: { width: 160, height: 64 } },
  { id: 'ship', label: 'Ship', position: { x: 320, y: 80 },
    size: { width: 160, height: 64 } },
];

export const edges: EdgeSpec[] = [
  { id: 'plan-ship', source: 'plan', target: 'ship',
    sourceHandle: 'right', targetHandle: 'left' },
];
```

## 2. Connect QRL callbacks and sibling subscriptions

Wrap the canvas and its toolbar in [`GrafloriaProvider`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaprovider). [`useGrafloria`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#usegrafloria) returns the shared instance signal; it is `undefined` until the flow mounts. [`useSelection`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#useselection) returns the current selected nodes and edges, and [`useViewport`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#useviewport) returns the camera's zoom and world origin.

Use the [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) signal to show canvas readiness beside the sibling toolbar; see the [Qwik quick start](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-quick-start) for `onInit$`, `noSerialize()` and QRL callback setup.

For a sibling side effect, [`useOnSelectionChange$`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#useonselectionchange) fires a QRL on each selection change and removes its subscription automatically. Here it counts selection notifications rather than storing live models in application state.

[`useOnSelectionChangeQrl`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#useonselectionchangeqrl) accepts an explicitly `$()`-wrapped handler. The second subscription logs selected node ids to the browser console.

```tsx title="editor.tsx"
import { $, component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import {
  GrafloriaFlow, GrafloriaProvider, useGrafloria,
  useSelection, useViewport, useOnSelectionChange$, useOnSelectionChangeQrl,
  type DiagramInstance,
} from '@grafloria/qwik';
import { nodes, edges } from './diagram-data';

const Toolbar = component$(() => {
  const instance = useGrafloria();
  const selection = useSelection();
  const viewport = useViewport();
  const notifications = useSignal(0);

  useOnSelectionChange$(() => {
    notifications.value += 1;
  });

  useOnSelectionChangeQrl($(({ nodes: selectedNodes }) => {
    console.log('Selected nodes:', selectedNodes.map((node) => node.id));
  }));

  return (
    <div>
      <button type="button" disabled={!instance.value}
        onClick$={() => instance.value?.fitView(40)}>
        Fit view
      </button>
      <p>
        Selected: {selection.value.nodes.length} node(s),{' '}
        {selection.value.edges.length} edge(s).{' '}
        Zoom: {viewport.value.zoom.toFixed(2)}.{' '}
        Origin: {viewport.value.x.toFixed(0)}, {viewport.value.y.toFixed(0)}.
      </p>
      <p>Selection notifications: {notifications.value}</p>
    </div>
  );
});

export default component$(() => {
  const instance = useSignal<NoSerialize<DiagramInstance>>();
  const clicked = useSignal('None');

  return (
    <GrafloriaProvider>
      <Toolbar />
      <p>Canvas: {instance.value ? 'Ready' : 'Starting'}. Last clicked node: {clicked.value}</p>
      <GrafloriaFlow
        defaultNodes={nodes}
        defaultEdges={edges}
        fitView
        style={{ height: '400px' }}
        onInit$={(diagram) => { instance.value = noSerialize(diagram); }}
        onNodeClick$={({ node }) => { clicked.value = node.id; }}
      />
    </GrafloriaProvider>
  );
});
```

The mounted canvas shows Plan connected to Ship. Click a node to update the clicked-node text and selection readout. Pan or zoom to update the camera readout; Fit view frames the content. `defaultNodes` and `defaultEdges` seed the instance once, so the instance owns subsequent edits.

![Plan connects to Ship beneath the Fit view button, selection and camera readouts, and Ready status. No node is selected.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/ae426f8eada442464e38f2f188564e8e.png)

For controlled state instead, pass `nodes` and `edges` from typed signals and write the returned specs back through `onNodesChange$` and `onEdgesChange$`. Those callbacks receive spec arrays, not the live models supplied by selection callbacks. See [State and event flow](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/state-and-event-flow) for the ownership decision.

## 3. Adopt the server SVG with its CSS

Pass the [`StaticRenderResult`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-core#staticrenderresult) from [`renderToStaticSVG`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-core#rendertostaticsvg) to the Qwik flow's `ssr` prop and emit its CSS separately to turn the server preview into an interactive canvas; see [React: state and subscriptions](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/react-state-and-subscriptions) for the static result's fields.

In an SSR-rendered component, build the result in `useTask$`, following the resumable demo. Keep that result as plain data and pass it to `ssr` together with the same node and edge specs. This example emits the stylesheet beside the flow; when composing your document shell, place `result.css` in a `<style>` in the document head so the first server response already carries the diagram's styles.

```tsx title="resumable-diagram.tsx"
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
import { GrafloriaFlow, renderToStaticSVG, type StaticRenderResult } from '@grafloria/qwik';
import { nodes, edges } from './diagram-data';

export default component$(() => {
  const result = useSignal<StaticRenderResult>();
  const ready = useSignal(false);

  useTask$(() => {
    result.value = renderToStaticSVG({
      nodes, edges, width: 640, height: 400,
      instanceId: 'plan-ship-ssr', fitView: true,
    });
  });

  return (
    <>
      <style dangerouslySetInnerHTML={result.value?.css ?? ''} />
      <p>{ready.value ? 'Interactive' : 'Server preview'}</p>
      <GrafloriaFlow
        defaultNodes={nodes}
        defaultEdges={edges}
        ssr={result.value}
        style={{ height: '400px' }}
        onInit$={() => { ready.value = true; }}
      />
    </>
  );
});
```

View the server response source: Plan, Ship and their SVG are already in the HTML. At document ready, the flow builds a live instance using the snapshot and adopts the existing DOM instead of rebuilding it. The status changes to Interactive, and you can drag the nodes. Resumption avoids re-walking the component tree; it does not defer the diagram's initialization until the first click.

![The Interactive status appears above Plan connected to Ship; the left edge of the Plan box is clipped by the canvas boundary.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/29c446c92ec1090acbb76c77b6dd5867.png)

The snapshot carries instance scope, canvas width and height, zoom, and viewport origin—not a saved document. The client rebuilds the model from your specs. Use [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) for persistence. Custom HTML nodes mount in the browser; they are not part of the server-rendered SVG.

## Options that matter

For the flow options below, see [`GrafloriaFlowProps`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik#grafloriaflowprops). Static rendering takes [`StaticRenderOptions`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-core#staticrenderoptions).

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `defaultNodes`, `defaultEdges` | `NodeSpec[]`, `EdgeSpec[]` | Empty arrays when no controlled inputs exist | Seed the instance on mount. |
| `nodes`, `edges` | `NodeSpec[]`, `EdgeSpec[]` | Unset | Reconcile controlled specs into the instance. Wire their change callbacks back to your state. |
| `ssr` | `{ html: string; snapshot: HydrationSnapshot }` | Unset | Emit server markup and adopt it in the browser. Supply its CSS separately. |
| Static `width`, `height` | `number` | `800`, `600` | Set the server canvas dimensions in CSS pixels. |
| Static `instanceId` | `string` | `'grafloria-ssr'` | Scope the diagram. Give multiple server-rendered diagrams distinct ids. |
| Static `fitView` | `boolean` | `false` | Frame content instead of using the supplied camera. |
| Static `fitPadding` | `number` | `40` | Set fit padding in CSS pixels. |

[`HydrationSnapshot`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-core#hydrationsnapshot) is returned by static rendering; pass it through rather than reconstructing it.

## Keep browser objects out of resumable state

The provider's instance signal is already marked `noSerialize()`. Your own instance signal needs the explicit marker shown above. Its value does not survive serialization: the browser builds a fresh instance, and `onInit$` supplies that instance again. Do not close over a live instance directly in another QRL; capture its signal and read `.value` when the handler runs.

Create live transports and shared stores in `useVisibleTask$`, retain them with `noSerialize()`, and tear them down when their owner unmounts. The flow's collaboration options are fixed for the instance's lifetime, so create the options before mounting a flow that consumes them. See [Documents and kits](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/documents-and-kits) for live-object handling and [Qwik: custom content and kits](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-custom-content-and-kits) for browser-built `spec$` factories.

Keep the provider above both the canvas and its subscribing siblings. Outside a provider, the hooks have no instance to reach; `useGrafloria()` stays `undefined` and logs a development warning. Use a separate provider for each independently controlled canvas.

## Load the event loader in client-only apps

An SSR page carries Qwik's event loader automatically. If your app only calls Qwik's `render()` in the browser, add the loader once before rendering; without it, DOM handlers such as Fit view do not fire.

Use this entry only for a client-only app. It mounts the editor from step 2 into a newly created element in the browser.

```tsx title="main.tsx"
import { render } from '@builder.io/qwik';
import { QWIK_LOADER } from '@builder.io/qwik/loader';
import Editor from './editor';

const loader = document.createElement('script');
loader.textContent = QWIK_LOADER;
document.head.appendChild(loader);

const container = document.createElement('div');
document.body.appendChild(container);
void render(container, <Editor />);
```

## Live demos and related tasks

- [Qwik gallery](https://grafloria.com/demos-qwik/) — run the framework's diagrams and inspect their source.
- [Server-side export](https://grafloria.com/demos/misc/server-side-export.html) — inspect a static SVG preview; unlike the adopted flow above, that preview is not interactive.
- [Resumable sample source](https://github.com/grafloria/grafloria/blob/cccb505e76c7af26c8bafecaba8146db7d8df928/apps/demos-qwik/src/demos/ssr-resumable.tsx) — the server markup and snapshot handoff.
- [Instance and lifecycle](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/instance-and-lifecycle) — instance operations and teardown.
- [Qwik: custom content and kits](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/qwik-custom-content-and-kits) — custom containers and QRL spec factories.
