Run resources standalone, with no React tree.
Resources don't need React. createResourceRoot hosts a resource tree
imperatively, which is how libraries and tests drive resources, and how
@assistant-ui/store bridges them into React.
createResourceRoot
import { createResourceRoot } from "@assistant-ui/tap";
const root = createResourceRoot();
const handle = root.render(Counter({ initialValue: 0 }));
// read the current value
handle.getValue().count; // 0
// subscribe to changes
const unsubscribe = handle.subscribe(() => {
console.log(handle.getValue().count);
});
// call methods
handle.getValue().increment();
// re-render with new props
root.render(Counter({ initialValue: 10 }));
// clean up
root.unmount();createResourceRoot() returns { render, unmount }. render(element) hosts (or
re-hosts) the element and returns a stable { getValue, subscribe } handle:
getValue() reads the resource's current return value, and subscribe(callback)
fires whenever it changes.
Scheduling and flushing
How updates are delivered depends on what hosts the tree:
| Host | Scheduler | Updates delivered via |
|---|---|---|
useResource | React | React re-render |
useResourceRoot | tap | .subscribe() |
createResourceRoot | tap | handle.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.
flushResourcesSync
flushResourcesSync flushes pending tap-scheduled updates synchronously, so the
new state is readable immediately after.
import { flushResourcesSync } from "@assistant-ui/tap";
flushResourcesSync(() => handle.getValue().increment());
console.log(handle.getValue().count); // already updatedThis applies to tap-scheduled trees (createResourceRoot / useResourceRoot). 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.
Controlling child re-renders
By default a child re-renders whenever its parent does. useResource accepts an
optional dependency array as a second argument to control when the child receives
new props, mirroring a useMemo dependency list:
const counter = useResource(Counter({ incrementBy }), [incrementBy]);This optimization is experimental and applies inside a resource render; it is
ignored when useResource runs in a React component.