Component State Exposure
An element registration tells a driver what is on the page. Component state tells it what the app thinks is true — the cart total, the form's validity, which tab is selected — without scraping it back out of the DOM.
You expose it with useUIComponent, and read it over
GET /control/component/:id/state.
Basic State Exposure
useUIComponent registers a component. Two of its options carry state: state
is a getter for the raw values, and computed is a map of derived values.
import { useState } from 'react';
import { useUIComponent } from '@qontinui/ui-bridge';
function Counter() {
const [count, setCount] = useState(0);
useUIComponent({
id: 'counter',
name: 'Counter',
state: () => ({ count }),
computed: {
isEven: () => count % 2 === 0,
},
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
state is a function, not a value — it is called at read time, so the
driver always sees the current render's values rather than a snapshot taken at
registration.
There is no useExposeState hook. State exposure is a property of a registered
component, which is what gives it an id, a name, actions and a lifecycle.
Reading Exposed State
GET /control/component/counter/state
{
"success": true,
"data": {
"state": { "count": 5 },
"computed": { "isEven": false },
"timestamp": 1717171717171
}
}
state and computed come back as separate maps — a ComponentStateResponse.
The route returns NOT_FOUND when no component with that id is registered, and
an empty pair when the component is registered but exposes no state getter.
Related reads: GET /control/components lists every registered component with
its state keys and actions, and GET /control/component/:id returns one
component's descriptor.
Form State
A form is the case this was built for — values, errors and validity in one read:
import { useState } from 'react';
import { useUIComponent } from '@qontinui/ui-bridge';
function LoginForm() {
const [values, setValues] = useState({ email: '', password: '' });
const [errors, setErrors] = useState<Record<string, string>>({});
useUIComponent({
id: 'login-form',
name: 'Login Form',
state: () => ({ values, errors }),
computed: {
isValid: () => Object.keys(errors).length === 0,
isDirty: () => values.email !== '' || values.password !== '',
},
actions: [
{
id: 'submit',
label: 'Submit login',
paramSchema: { email: 'string', password: 'string' },
handler: async (params: { email: string; password: string }) => submitLogin(params),
},
],
});
return <form>…</form>;
}
Actions declared alongside the state are callable over
POST /control/component/login-form/action/submit, so a driver can read the
state, decide, and act through the same component.
For DOM-level form analysis that needs no registration at all, use
GET /control/forms (plus POST /control/forms/snapshot and
/control/forms/diff), or GET /ai/snapshot?includeForms=true — see
Visual Context.
Namespacing
Component ids are flat strings, so namespace by convention with a separator you choose. Each namespace is a separate component with its own lifecycle:
useUIComponent({ id: 'user-profile.details', name: 'Profile details', state: () => ({ name, email }) });
useUIComponent({ id: 'user-profile.preferences', name: 'Profile preferences', state: () => ({ theme }) });
GET /control/component/user-profile.details/state
There is no hierarchy in the registry: user-profile.details is an opaque id,
not a path, and reading user-profile will not aggregate its children.
UI States vs Component State
These are two different mechanisms and it is worth not confusing them:
- Component state — arbitrary app values, per component, read with
GET /control/component/:id/state. - UI states — named conditions of the interface (
logged-out,cart-open), registered withuseUIState, used for pathfinding and navigation.
import { useUIState } from '@qontinui/ui-bridge';
function CartDrawer({ open }: { open: boolean }) {
const { isActive } = useUIState({
id: 'cart-open',
name: 'Cart Open',
activeWhen: () => open,
blocking: true,
});
return isActive ? <aside>…</aside> : null;
}
GET /control/states/snapshot returns a StateSnapshot
({ timestamp, activeStates, states, groups, transitions }) — the state-machine
picture, not a dump of every component's state. See
State Machine Integration.
Watching for Changes
There is no watchState / waitForState / diffState on the client. Component
state is read on demand, and change detection is a separate, semantic
mechanism:
-
POST /ai/wait-for-change— block until a predicate is satisfied, and get the semantic diff that satisfied it:POST /ai/wait-for-change
{
"predicate": { "textContains": { "elementId": "cart-total", "text": "$42" } },
"options": { "timeout": 10000, "interval": 200 }
}A
ChangePredicatecan beelementAppeared,elementDisappeared,propertyChanged,textContains,categoryoranySignificantChange. -
GET /ai/diff— the semantic diff since the previous call. -
GET /control/changes/since— the push-based change log. -
WebSocket events — subscribe to
element:stateChanged,action:completedand the rest. Note that there is nocomponent:stateChangedevent: components emitcomponent:registeredandcomponent:unregisteredonly, so a component whose state changes without re-registering will not push a notification. PollGET /control/component/:id/state, or watch the element-level signals that the state change causes. See WebSocket Communication.
To compare two points in time, take a snapshot before and after and diff those:
import { createSnapshotManager, createDiffManager, getGlobalRegistry } from '@qontinui/ui-bridge';
const registry = getGlobalRegistry();
const snapshots = createSnapshotManager();
const diffs = createDiffManager();
diffs.update(snapshots.createSnapshot(registry.createSnapshot()));
await doSomething();
const diff = diffs.update(snapshots.createSnapshot(registry.createSnapshot()));