Why tap exists and when to reach for it.
React hooks are great, until your data doesn't match your UI
React hooks let you colocate state and effects with the component that uses them.
This works beautifully when the shape of your data matches the shape of your UI. A
<Counter> component owns a count state. A <Form> component owns its input
states. Simple.
But over the lifetime of any non-trivial app, data needs to be accessed from multiple places in the UI. So you hoist state up. Then up again. Eventually, most of your state lives in a store at the root of your app: global state.
You can't use hooks for global state
Once state is global, hooks can't manage it. You can't call useState in a loop
for each message in a thread, that would break the rules of hooks. The very
paradigm that made React so pleasant doesn't apply to your most important state.
This is why state management libraries are so popular. Zustand, Jotai, Redux, and MobX all facilitate this migration into global stores and let you efficiently access that state from anywhere in your app.
Hooks, without the UI
tap brings useState, useEffect, useMemo, and the rest outside of React
components and into standalone resources. Same rules of hooks, same mental
model, just not tied to the UI tree. A resource owns state the way a component
does, but returns a plain value instead of JSX, so you can use it for the global
state hooks normally can't reach.
Lifecycle
Unlike Zustand, Jotai, and RxJS, tap has a concept of lifecycle: render and
commit phases, mount and unmount, and useEffect. That lets resources handle
server-side rendering, Suspense, hydration, mounting, and cleanup the same way a
React component does.
Composable configuration
tap shapes assistant-ui's API so that every piece of configuration is a composable resource:
const aui = useAui({
cloud: AssistantCloud({ apiKey: "..." }),
threads: CloudThreadList({
thread: () => AISDKThread({ transport: /* ... */ }),
}),
});Each of these — AssistantCloud(...), CloudThreadList(...), AISDKThread(...) — returns a ResourceElement: a simple { hook, args } object that carries both the implementation and its configuration. Users can swap, compose, and nest them freely.
Architecture
tap ships as two packages:
@assistant-ui/tap: the hooks engine, general-purpose and not tied to assistant-ui.@assistant-ui/store: the glue between tap and React, connecting resources to components viauseSyncExternalStoreand React context.