How resource trees form, re-render, and get scheduled.
Resource trees
When resources render other resources via useResource or useResources, they form a tree:
createTapRoot(function AppRoot() { return useResource(App()); }) <- tree root
├─ useResource(Sidebar)
│ └─ useResource(NavItem)
└─ useResource(Main)
├─ useResource(Header)
└─ useResources([...Items])Re-renders
This is the biggest difference from React.
In React, a state change re-renders only the component that changed and its children. Parents are unaffected.
In tap, the entire resource tree re-renders from the root. Because useResource returns the child's value directly to the parent, the parent must re-run to receive the updated value, which means its parent must re-run too, all the way up to the tree root.
useTapRoot breaks this chain: it creates a subtree boundary. Everything below it becomes a separate tree that re-renders independently. The parent doesn't re-render when the subtree updates.
Tree roots and scheduling
Every tree has a root that determines how updates are scheduled and delivered:
| Root | Scheduler | Updates delivered via |
|---|---|---|
useResource | React | React re-render |
createTapRoot | Tap | root.subscribe() |
useTapRoot | Tap | .subscribe() |
React scheduler: batching, priority, and timing are all controlled by React.
Tap scheduler: state changes are batched using microtasks. Multiple setState calls in the same synchronous block result in a single re-render.
// tap scheduler: only one re-render, not two
setCount(1);
setName("hello");If updates keep triggering more updates (e.g. a useEffect that calls setState), tap will flush up to 50 times before throwing a maximum update depth error.
flushTapSync
flushTapSync lets you flush pending tap scheduler updates synchronously.
import { flushTapSync } from "@assistant-ui/tap";
flushTapSync(() => {
handle.getValue().increment();
});
// state is already updated here
console.log(handle.getValue().count);This only applies to tap-scheduled trees (createTapRoot / useTapRoot). For useResource trees, use flushSync from react-dom instead.
This is useful when a library expects a result synchronously, for example, React's controlled inputs need the store to update within the onChange handler, otherwise React reverts the input.