Trees & Re-renders

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:

RootSchedulerUpdates delivered via
useResourceReactReact re-render
createTapRootTaproot.subscribe()
useTapRootTap.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.