Outside React

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:

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

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