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.

Use a hookCall the SDK directly
Resources you list and mutate — useTasksOne-shot actions — sendTextToChat, openProject, notifications.show
Long-running work with state — useSpecialistPure computation over data you already hold
Subscriptions and long-lived stateReads a caller already awaits once

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.

FieldMeaning
isSupportedThe host exposes this capability at all. Hide the affordance when false.
isLoadingA first load is in flight — show a skeleton once.
isBusyAny read or write is in flight — disable controls.
failure{ reason, message, detail? }, or absent. message is safe to show.
reload()Re-read from the host.

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, not delete. delete is a reserved word, so const { 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.

import { useSpecialist } from '@theaiplatform/miniapp-sdk/react';

// The whole call, when you just want an answer:
const drafter = useSpecialist('standup-drafter@0.2.0');

// Or with options — every one of them optional:
const typed = useSpecialist({
  specialistId: 'standup-drafter@0.2.0',
  parse: (text) => Draft.safeParse(JSON.parse(text)).data,
});

if (!drafter.isSupported) return null;
return (
  <>
    <button
      disabled={drafter.isRunning}
      onClick={() => drafter.run('Draft my standup.')}
    >
      {drafter.isRunning ? 'Drafting…' : 'Draft'}
    </button>
    {drafter.failure && <p role="alert">{drafter.failure.message}</p>}
    {drafter.data && (
      <Draft value={drafter.data} onRetry={drafter.regenerate} />
    )}
  </>
);

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.

const evaluator = useSpecialist({ ...shared, specialistId: evaluatorId });
const stakeholder = useSpecialist({ ...shared, specialistId: stakeholderId });

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

const {
  data: tasks,
  isLoading,
  isBusy,
  failure,
  find,
  create,
  update,
  remove,
} = 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.