Outside React

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:

HostSchedulerUpdates delivered via
useResourceReactReact re-render
useTapRoottap.subscribe()
createTapRoottaproot.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 updated

This 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.