React Hooks
@theaiplatform/miniapp-sdk/react wraps the platform API in React hooks so a
surface does not hand-roll loading state, failure classification, and refresh for
every capability it touches.
Hooks live in their own entry rather than under ./ui, because ./ui is the
design system — components and stylesheets. A surface that only wants a task list
should not pull the component library.
What has a hook, and what does not
A hook earns its place when there is state to manage, a subscription to hold, or a
multi-step sequence to run. It is not a goal to wrap everything: for a
fire-and-forget call, sdk.chat.sendTextToChat(text) is clearer than any wrapper
around it, and every hook is public API that must be maintained and versioned.
Nothing needs passing in beyond the specialist you want. Each hook reads the
installed sdk itself, the workspace defaults to the one your surface is mounted
in, and the model defaults to the workspace's choice — pinning a model in app code
is usually a mistake. Supply a
capability explicitly only to inject a fake in a test. Reading it before the host
has installed the API reports unsupported rather than throwing, so a surface that
renders early degrades instead of crashing.
Every hook reports failures the same way: an enumerated reason, a message safe
to show, and an optional detail for logs. A withheld grant is always denied,
and anything unrecognized is never reported as a denial — telling someone "not
permitted" during a backend outage sends them to fix permissions they already
have.
The shape every hook returns
Hooks share one vocabulary so moving between them requires no relearning.
Resource hooks add data for the collection, find(id) for a synchronous lookup
in what is already loaded, and create / update / remove for writes. Writes
resolve with the affected item — or undefined/false on failure, with the cause
in failure — and reload rather than patching local state, so a surface never
shows something the host would describe differently.
Two naming choices worth knowing, both forced rather than stylistic:
data, not the domain name. One shared shape across hooks. Rename it where a domain name reads better:const { data: tasks } = useTasks().remove, notdelete.deleteis a reserved word, soconst { delete } = useTasks()will not parse.
Not every resource supports every operation, because the platform APIs do not.
Tasks have a list but no by-id read, which is why find searches loaded data
instead of fetching; projects have a by-id read but no list. A hook exposes only
what its capability actually provides, so a missing method means a missing platform
operation rather than an oversight.
No shared cache yet
Each hook fetches independently, so two components calling useTasks fetch twice
and can briefly disagree. Hoist the hook and pass results down when that matters.
A shared cache is a deliberate open decision, not an oversight — adopting one
changes every hook's semantics, so it should happen once rather than per hook.
useSpecialist
Runs a turn against a specialist your package declares. See Specialists for the declaration contract and the channel-versus-room model.
status is unsupported, idle, running, ready, or failed. A failure
outranks a previous answer, so a failed regenerate never leaves a stale result
presented as current. While a turn is in flight run is a no-op, which matches the
host serializing turns on one room.
parse types the result: return null or undefined to reject an answer as
off-contract — both, so a parser using either convention needs no adapter.
Omit it and data is the raw text.
run, regenerate, and reset keep one identity for the hook's whole life, so
they are safe in a dependency array and as a memoized child's prop. The in-flight
guard tracks the turn rather than the render, which is also why reset() does not
let a second turn start while the host is still working on the first.
Several specialists. Call the hook once per specialist — the same shape as one query per key. Each gets its own room keyed on (workspace, package, specialist), so they neither share history nor queue behind each other.
No cancellation. The host offers a guest no way to stop a turn it started, so
an abort() that only stopped the caller listening would be misleading while the
turn kept running.
The same lifecycle without React is runSpecialist from
@theaiplatform/miniapp-sdk/sdk.
useTasks
Writes reload rather than patching local state. The host owns task shape — status normalization, assignee resolution, archived transitions — so echoing a locally-mutated task risks showing something the host would describe differently.
isLoading covers the first load, for a skeleton; isBusy covers any read or
write, for disabling controls.