React bindings
./react is an optional entry point (decision-18):
it is the only subpath that imports React, react is an optional peer, and
the root, ./bridge and ./host stay free of any framework or browser global
(tests/surface.test.ts bundles every entry and fails on a stray framework;
tests/consumer/isolation.ts walks dist/ after every build and fails when
./react reaches vue or svelte, or the other way round). The bindings add
no second client and no behaviour of their own: every hook is
useSyncExternalStore over one of the stores.
A BridgeProvider shares the QgisSdk a vanilla page builds (or, for the
legacy client, its BridgeClient), and the hooks take the instance’s methods,
so the typed contract is the generated one.
import { QgisSdk } from "@archont561/qgis-sdk";import { BridgeProvider, useOperation, useQgisSdk, useStream } from "@archont561/qgis-sdk/react";import schema, { type Operations, type Streams } from "./bridge.schema";
const qgis = QgisSdk.fromBridgeSchema(schema);
function App() { // Shares the instance the page owns; the provider never closes it. Pass // `transport` instead to let the provider own a client it closes on // unmount, or `client` to share a bare BridgeClient. Exactly one of the // three is required. return createElement(BridgeProvider, { qgis }, createElement(Sites));}
function Sites() { const qgis = useQgisSdk<Operations, Streams>(); const [summary, loadSummary] = useOperation(qgis.sites.summary); const watch = useStream(qgis.sites.watch, { layer_id: "sites" }, { onEvent: (event) => { if (event.kind === "data") render(event.payload.change); }, }); // summary.status: idle | pending | ok (value) | error (error) // watch.status: idle | opening | open | ended (terminal) | failed (error)}useOperations(createOperations) remains for the 0.3.0 generated client and
is memoised per provider client, as before.
The form and the layer picker, as the React scaffold renders them:
import { layersQuery } from "@archont561/qgis-sdk";import { useForm, useHostQuery, useQgisSdk } from "@archont561/qgis-sdk/react";
function BufferForm() { const qgis = useQgisSdk<Operations, Streams>(); const [state, form] = useForm(qgis, "buffer", { initial: { distance_metres: 500 } }); const [layers] = useHostQuery(useMemo(() => layersQuery(qgis), [qgis])); // form.fields -> one <input> per declared field; form.set / form.submit // layers.status === "refused" -> tell the author which grant is missing}-
useStore(store)subscribes a component to anyStore<T>. -
useForm(qgis, actionId, options?)returns[FormState, FormStore]; the store is created once per(qgis, actionId)and aborted on unmount. -
useHostQuery(store, { auto? })returns[HostQueryState, store]and refreshes once on mount unlessauto: false. -
useOperation(fn)returns[state, run, { abort, reset }].run(input)resolves with the value, or withundefinedwhen the call failed, was aborted or was superseded - the state carries the error, so the promise never rejects into an event handler. Only the latest run can change the state; unmounting aborts whatever is in flight and settles nothing afterwards. -
useStream(open, input | null, handlers)keeps one subscription open while the component is mounted. It follows the content ofinput(a new object with equal fields does not reopen it),nullcloses it, handlers are read fresh on every frame, and unmount closes the stream even when the host has not acknowledged it yet. A host terminal moves the state toended; a refused subscription isfailedwith theQgisSdkBridgeRemoteError. -
StrictMode. The simulated remount closes the owned client and the hook subscriptions; the effects that follow make exactly one new client and one new subscription, which the tests assert through the harness below.
-
Unmounting the provider closes an owned client, which detaches the transport; a closed client sends no unsubscribe, the view going away is what disposes its subscriptions host-side (decision-23). Unmounting a component under a live provider does send one.