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.