Run resources standalone, with no React tree.
Resources don't need React. createTapRoot hosts a resource tree
imperatively, which is how libraries and tests drive resources, and how
@assistant-ui/store bridges them into React.
createTapRoot
import { createTapRoot, useResource } from "@assistant-ui/tap";
const root = createTapRoot(function CounterRoot() {
return useResource(Counter({ initialValue: 0 }));
});
// read the current value
root.getValue().count; // 0
// subscribe to changes
const unsubscribe = root.subscribe(() => {
console.log(root.getValue().count);
});
// call methods
root.getValue().increment();
// clean up
root.unmount();createTapRoot(callback) runs the callback as the root's render body, in which you
call useResource to host a resource. It returns a stable
{ getValue, subscribe, unmount } object: getValue() reads the resource's
current return value, subscribe(callback) fires whenever it changes, and
unmount() tears the tree down.
Scheduling and flushing
How updates are delivered depends on what hosts the tree:
| Host | Scheduler | Updates delivered via |
|---|---|---|
useResource | React | React re-render |
useTapRoot | tap | .subscribe() |
createTapRoot | tap | root.subscribe() |
The tap scheduler batches state changes: multiple setters in the same synchronous block produce a single re-render. If updates keep triggering more updates (for example, an effect that sets state), tap flushes up to 50 times before throwing a maximum-update-depth error.
flushTapSync
flushTapSync flushes pending tap-scheduled updates synchronously, so the
new state is readable immediately after.
import { flushTapSync } from "@assistant-ui/tap";
flushTapSync(() => root.getValue().increment());
console.log(root.getValue().count); // already updatedThis applies to tap-scheduled trees (createTapRoot / useTapRoot). For
useResource trees, use flushSync from react-dom instead. It is useful when a
library expects a synchronous result, such as a controlled input that needs its
store updated inside the onChange handler.
React interop
When a resource is hosted via useResource inside React, tap integrates with
React's scheduler:
- Concurrent features (
startTransition,useDeferredValue,<Suspense>) work without tearing. <Activity>: updates that arrive while a resource is hidden are tracked and replayed when it becomes visible again.- Strict mode is inherited from the surrounding
<StrictMode>.
No special configuration is needed.
@assistant-ui/store bridges resources into React with useSyncExternalStore, so
those reads trigger synchronous re-renders and opt out of concurrent scheduling.
This may change in a future release.