Skip to main content

Integration Hooks

Three hooks from @qontinui/ui-bridge/react connect the SDK to the world outside the React tree: a command relay on your server, an automation driver reading snapshots, and a deployment that ships new bundles under a running tab.

useCommandRelay​

function useCommandRelay(options?: UseCommandRelayOptions): void

Connects the browser to the server's command relay. It opens an SSE stream, executes each command it receives against the UI Bridge registry and browser APIs, POSTs the results back, and heartbeats so the server knows the tab is alive. Mount it once, near your app root, below the UIBridgeProvider.

import { useCommandRelay } from '@qontinui/ui-bridge/react';

function BridgeRelay() {
useCommandRelay({
basePath: '/api/ui-bridge',
authHeader: () => sessionStorage.getItem('sessionToken'),
registrationMetadata: () => ({ userId: currentUser.id, sessionId: currentSession.id }),
});
return null;
}

The transport uses three routes under basePath: GET {basePath}/commands/stream (SSE), POST {basePath}/commands (results) and POST {basePath}/heartbeat. It reconnects on visibility change. See Server Overview for the other end.

Options​

UseCommandRelayOptions — every field is optional, as is the argument itself.

OptionTypeDefaultMeaning
enabledbooleantrueTurn the relay off without unmounting
basePathstring'/api/ui-bridge'Base path for the three relay routes
heartbeatIntervalnumber10000Heartbeat period in ms
runnerUrlstring'http://127.0.0.1:9876'Explicit runner URL for phone-home registration
disablePhoneHomebooleanfalseSkip phone-home registration entirely
appIdstringlocation.host (port-inclusive, e.g. localhost:3000)Stable identity in the runner's registry
appNamestringdocument.title || location.hostnameDisplay name
appType'web' | 'desktop' | 'mobile' | 'dashboard' | 'other''web'App classification
frameworkstring'react'Framework hint
capabilitiesstring[]['control']Capability tags
versionstring—SDK/app version reported on heartbeats
authHeader() => string | null | undefined—Per-request bearer token source
registrationMetadata() => { userId, sessionId } | null | undefined—Per-user tab scoping; see below

runnerUrl also changes when phone-home fires: with it set, registration is attempted regardless of hostname; without it, phone-home is gated to localhost-family hosts.

The default appId is location.host, which includes the port, so two dev apps on localhost:3000 and localhost:3001 register as two apps. If the runner refuses the registration (HTTP 409 / 403, e.g. UIB_REGISTRATION_HELD when another live app already holds the id), the hook logs one console.warn per mount naming the refusal code; pass an explicit appId to resolve it.

authHeader​

Return the raw token, without a Bearer prefix — the SDK adds it. When the function returns a non-empty string, Authorization: Bearer <value> is attached to all three routes, including the SSE stream (consumed via fetch streaming, so no token ever lands in a URL).

It is called fresh on every outbound request, so read from a live source such as sessionStorage and a rotated token is picked up without remounting. Returning null, undefined or '' means "no auth this call" and the transport falls back to unauthenticated / cookie-based behaviour.

Supply it for any relay that enforces a session-bound auth gate — for example qontinui-web's UI_BRIDGE_REQUIRE_AUTH=1 mode.

registrationMetadata is effectively required against a strict relay

As of @qontinui/ui-bridge 0.12 the server rejects any POST /heartbeat whose body omits this field. A tab without registration metadata never enters the ownership registry, so authenticated dispatch cannot reach it at all. Older relays simply ignore the extra field, so supplying it is always safe.

When present, the server uses { userId, sessionId } to record tab ownership, filter /tabs and /tabs/wait to the caller's own tabs, reject cross-user targetTabId dispatch with a 404, and scope unscoped fan-out to the caller's tabs. Like authHeader it is a function, read fresh on every beat, so a re-login can rotate sessionId without remounting. Returning null, undefined, or a value missing either field means "no metadata this beat" — and a strict server answers 400.

useUIBridgeEcho​

function useUIBridgeEcho<T>(id: string, value: T, options?: UseUIBridgeEchoOptions<T>): ReactElement

Surfaces arbitrary state in the snapshot by registering a hidden read-only <input> whose value is the serialised state. Unlike the other hooks here it returns a React element, which you must render.

import { useUIBridgeEcho } from '@qontinui/ui-bridge/react';

function CaptureHost() {
const [bbox, setBbox] = useState<Bbox | null>(null);
const echo = useUIBridgeEcho('capture-last-bbox', bbox);

return (
<>
<iframe title="target" src="/target" />
{echo}
</>
);
}

A driver then reads it out of /control/snapshot:

snap = client.get_control_snapshot()
for el in snap['elements']:
if el['id'] == 'capture-last-bbox':
bbox = json.loads(el['state']['value'])

This exists because the snapshot does not expose <body> data-attributes, so mirroring state there is invisible to automation. Registering an input via useUIElement puts the value on element.state.value, where a driver can see it.

Parameters​

ParameterTypeMeaning
idstringUI Bridge element id; must be unique on the page
valueTJSON-compatible state to echo
options.labelstringLabel exposed via UI Bridge. Default: `UI Bridge echo: ${id}`
options.serialize(value: T) => stringCustom serializer. Default JSON.stringify
options.onNullish'empty' | 'serialize'null/undefined handling. Default 'empty'
options.styleReact.CSSPropertiesMerged over the default hidden style

With onNullish: 'empty' (the default) a nullish value echoes ''; with 'serialize' it goes through the serializer, so JSON.stringify yields the string "null". A serializer that throws — a BigInt, a circular reference — is caught and echoes '' rather than breaking the render.

Do not make the input invisible

The default style is deliberate: 2×2 pixels at opacity: 0.01, absolutely positioned, pointer-events: none. It is nearly invisible while keeping non-zero geometry, because the auto-registrar's visibility filter skips zero-area elements — a conventional sr-only style with clip: rect(0 0 0 0) would drop the element from the snapshot and silently defeat the whole hook.

An options.style override is merged over these defaults, so overriding width, height, display or opacity can re-introduce exactly that failure.

useBuildIdWatcher​

function useBuildIdWatcher(options?: UseBuildIdWatcherOptions): void

Detects when the server has shipped a new bundle while the tab is still running the old code, so you can prompt for a refresh. Pairs with a server that injects <meta name="build-id" content="…"> into the served HTML and exposes the current build-id on a live source.

import { useState } from 'react';
import { useBuildIdWatcher } from '@qontinui/ui-bridge/react';

function BuildRefreshBanner() {
const [stale, setStale] = useState(false);
useBuildIdWatcher({ onBuildIdChange: () => setStale(true) });

if (!stale) return null;
return (
<div role="status" aria-live="polite">
A new build is available.
<button onClick={() => window.location.reload()}>Refresh</button>
</div>
);
}
The watched source must be able to change while the page is open

Comparing two compile-time constants of the same process — for example a meta tag baked into HTML embedded in a desktop binary against that same binary's compiled-in build-id — is a permanent false positive, not a staleness check. Replacing the executable on disk changes neither value, so the comparison can only ever fire on a build-time inconsistency, and the reload it prompts is a guaranteed no-op.

The qontinui runner shipped exactly that (a Tauri invoke custom getter against its own embedded meta tag); its banner never cleared and was deleted rather than repaired. Use this hook only where a real server — or another source that genuinely moves at runtime — is on the other end.

Options​

OptionTypeDefaultMeaning
healthStreamUrlstring'/health/stream'SSE stream emitting a buildId field per event
pollUrlstring—URL GET-ed periodically; body must be JSON with a top-level buildId
getCurrentBuildId() => Promise<string> | string—Custom getter
pollIntervalMsnumber30000Interval for pollUrl / getCurrentBuildId; 0 = one-shot on mount
onBuildIdChange(oldId: string, newId: string) => void—Fires at most once per mount

Exactly one source is used, in precedence order: getCurrentBuildId, then pollUrl, then the default SSE stream. pollIntervalMs is ignored on the SSE path.

Sources​

// SSE (default) — supervisor dashboard pattern
useBuildIdWatcher({ onBuildIdChange: () => setStale(true) });

// Polling — Next.js / qontinui-web pattern
useBuildIdWatcher({
pollUrl: '/api/health',
pollIntervalMs: 30_000,
onBuildIdChange: () => setStale(true),
});

// Custom getter — any source that changes at RUNTIME (see the warning above)
useBuildIdWatcher({
getCurrentBuildId: () => fetchBuildIdFromSomewhereLive(),
pollIntervalMs: 30_000,
onBuildIdChange: () => setStale(true),
});

The baseline is read once at mount from <meta name="build-id">. The hook no-ops cleanly when that tag is missing or empty, and when the chosen source is unavailable (EventSource undefined in SSR, fetch undefined outside a browser). onBuildIdChange fires at most once per mount; the source is torn down on unmount.