API Reference

Public API exported from @assistant-ui/tap.

resource

Turns a Hook into a resource factory.

function resource<R, A extends readonly unknown[]>(
  hook: (...args: A) => R,
): Resource<R, A>;

The Hook should use a use-prefixed name so the rules of Hooks lint its body.

import { resource } from "@assistant-ui/tap";
import { useState } from "react";

const useCounter = (props: { initialValue: number }) => {
  const [count, setCount] = useState(props.initialValue);
  return { count, increment: () => setCount((c) => c + 1) };
};

const Counter = resource(useCounter);

Calling the factory produces a ResourceElement.

withKey

Attaches a key to a resource element or resource factory.

function withKey<E extends ResourceElement<any>>(
  key: string | number,
  element: E,
  deps?: readonly unknown[],
): E;

function withKey<F extends Resource<any, any[]>>(
  key: string | number,
  resource: F,
): F;

Keyed elements preserve their identity in useResources. Keys in the same list must be unique.

import { withKey } from "@assistant-ui/tap";

const element = withKey("my-key", Counter({ initialValue: 0 }));

Warning

Passing a resource factory to withKey is experimental and may change.

Passing a resource factory returns a factory that adds the key to every element it creates:

const KeyedCounter = withKey("my-key", Counter);
const element = KeyedCounter({ initialValue: 0 });

Warning

Passing deps to withKey is experimental and may change.

The optional deps array applies only to an element. In useResources, an element with unchanged dependencies can reuse its last committed value without rendering again. State updates and context changes still render it.

Hooks

Inside a resource body you use React's own hooks, imported from "react": useState, useReducer, useEffect, useMemo, useCallback, useRef, useLayoutEffect, useInsertionEffect, useEffectEvent, useSyncExternalStore, useDebugValue, useContext, and use (tap contexts only — promises are not supported). They follow React's rules of Hooks, with a few differences. Hooks compiled by React Compiler also work: tap implements the compiler's memo-cache primitive.

The remaining React hooks are not supported inside a resource body: useId, useTransition, useDeferredValue, useImperativeHandle, useOptimistic, and useActionState. Calling one inside a resource throws a TypeError naming the hook, for example useId is not a function. This restricts only the resource body itself: a React component hosting a resource can still wrap useResource in concurrent features like useDeferredValue (lifecycle).

The Hooks below are exported by tap.

useResource

Hosts a single resource element and returns its value. It works inside either a React component or another resource.

function useResource<V>(element: ResourceElement<V>): V;
const value = useResource(Counter({ initialValue: 0 }));

useResources

Hosts a dynamic list of resource elements and returns their values in the same order. Every element must have a unique key attached with withKey. It works inside either a React component or another resource.

function useResources<V>(
  elements: readonly ResourceElement<V>[],
): V[];
const values = useResources(
  items.map((item) => withKey(item.id, Item({ text: item.text }))),
);

useTapRoot

Hosts a render callback behind a subscribable boundary. It returns a stable root handle instead of the render value, and works inside either a React component or another resource.

function useTapRoot<R>(render: () => R): {
  getValue(): R;
  subscribe(listener: () => void): () => void;
};

Use a named function so the rules of Hooks lint the callback body. See Trees & Re-renders.

const handle = useTapRoot(function CounterRoot() {
  return useResource(Counter({ initialValue: 0 }));
});
handle.getValue();
handle.subscribe(() => {});

useTapHost

Hosts a resource render callback inside a React component.

function useTapHost<R>(render: () => R): {
  value: R;
  effects: () => void;
};

The resource renders with the component and commits in a passive effect. effects is the per-render commit callback, not a Hook. The host calls it automatically. A descendant can also pass it to a dependency-less useEffect to commit before that descendant's other effects; the first call commits the render.

function CounterProvider({ children }: { children: ReactNode }) {
  const { value, effects } = useTapHost(function CounterHost() {
    return useResource(Counter({ initialValue: 0 }));
  });
  return (
    <CounterContext.Provider value={value}>
      <TapEffects effects={effects} />
      {children}
    </CounterContext.Provider>
  );
}

function TapEffects({ effects }: { effects: () => void }) {
  useEffect(effects);
  return null;
}

createTapRoot

Hosts a render callback outside React.

function createTapRoot<R>(
  render: () => R,
  options?: { mountOnSubscribe?: boolean },
): {
  getValue(): R;
  subscribe(listener: () => void): () => void;
  unmount(): void;
};
const root = createTapRoot(function CounterRoot() {
  return useResource(Counter({ initialValue: 0 }));
});
root.getValue();
root.subscribe(() => {});
root.unmount();

With mountOnSubscribe: true the lifecycle is derived from subscribers instead: the root renders lazily on the first getValue() or subscribe(), effects commit when the first subscriber attaches, and after the last unsubscriber the root soft-unmounts on the next scheduler flush — effect cleanups run, but state is preserved and the next subscriber remounts it. Because the unmount is deferred, an unsubscribe followed by a resubscribe in the same tick (React StrictMode's subscription bounce, useSyncExternalStore churn, a subscribing component remounting) is absorbed without touching effects. unmount() throws in this mode.

flushTapSync

Runs a callback and synchronously flushes the tap updates it schedules. The callback's return value is returned.

function flushTapSync<T>(callback: () => T): T;

This applies to tap-scheduled trees created by createTapRoot or useTapRoot. For a resource hosted directly in React, use flushSync from react-dom.

flushTapSync(() => handle.getValue().increment());

Context

Create a context with createContext from "react". Read it with use or useContext from "react".

const ThemeContext = createContext("light");

useContextProvider

Provides a React context value to every resource rendered inside a callback and returns the callback's value.

function useContextProvider<T, R>(
  context: React.Context<T>,
  value: T,
  render: () => R,
): R;
useContextProvider(ThemeContext, "dark", () => useResource(Button()));

Types

Resource

type Resource<R, A extends readonly unknown[] = any[]> = (
  ...args: A
) => ResourceElement<R>;

The factory returned by resource(). R is the Hook's return value and A is its argument tuple. For a Hook with one props object, A is [Props].

ResourceElement

type ResourceElement<R> = {
  readonly hook: (...args: any[]) => R;
  readonly args: readonly unknown[];
  readonly key?: string | number;
  readonly deps?: readonly unknown[];
};

An inert description of a resource to host. resource() supplies hook and args; withKey can add key and deps.