Motivation

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 via useSyncExternalStore and React context.