Trees & Re-renders

How resource trees form, re-render, and get scheduled.

Resource trees

When resources render other resources via tapResource or tapResources, they form a tree:

root.render(App())                <- tree root
  ├─ tapResource(Sidebar)
  │    └─ tapResource(NavItem)
  └─ tapResource(Main)
       ├─ tapResource(Header)
       └─ tapResources([...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 tapResource 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.

tapResourceRoot 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:

RootSchedulerUpdates delivered via
useResourceReactReact re-render
createResourceRootTaphandle.subscribe()
tapResourceRootTap.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 tapEffect that calls setState), tap will flush up to 50 times before throwing a maximum update depth error.

flushResourcesSync

flushResourcesSync lets you flush pending tap scheduler updates synchronously.

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

flushResourcesSync(() => {
  handle.getValue().increment();
});

// state is already updated here
console.log(handle.getValue().count);

This only applies to tap-scheduled trees (createResourceRoot / tapResourceRoot). 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.