Define resources, pass props, add keys, and render them.
A resource is a self-contained unit of reactive state and logic — like a React component, but without UI.
Defining a resource
You define a resource with the resource function.
import { resource, tapState } from "@assistant-ui/tap";
const Counter = resource(({ initialValue = 0 }: { initialValue?: number }) => {
const [count, setCount] = tapState(initialValue);
return {
count,
increment: () => setCount((c) => c + 1),
};
});resource() returns a factory function. Calling the factory creates a ResourceElement — a lightweight description of what to render, not an active instance yet.
ResourceElements
A ResourceElement is a simple { type, props } object — the same idea as a React JSX element ({ type, props } under the hood), but without JSX syntax.
const element = Counter({ initialValue: 10 });
// { type: Counter, props: { initialValue: 10 } }We deliberately avoided JSX for resource elements. In our testing, JSX confused users because it looked like UI code but wasn't rendering anything visible. Instead, we use a calling convention inspired by Flutter — ResourceName({ props }) — so that it reads like normal function calls.
Just like in React, a ResourceElement is inert. It doesn't do anything on its own — it's a description of what to render, not an active instance. See Instances for how to bring them to life.
Props
Resources can accept props, just like React components. In the example above, Counter takes an initialValue prop.
Props are passed to a resource instance by its owner — which can be another resource (via tapResource), a React component (via useResource), or imperative code (via createResourceRoot).
When a resource re-renders with new props, hooks like tapEffect and tapMemo can react to the changes through their dependency arrays.
Return value
Resources can return a value. This is how you expose state and methods to the outside world. Unlike React components which return JSX nodes, resources can return any JavaScript value — objects, arrays, numbers, strings, or anything else.
The return value is what you get when you read from an instance — directly from useResource in React, or via handle.getValue() with createResourceRoot.
Keys
You can attach a stable key to a ResourceElement with withKey. Keys are used to preserve identity when rendering lists with tapResources.
import { withKey } from "@assistant-ui/tap";
const element = withKey("my-counter", Counter({ initialValue: 10 }));
// { type: Counter, props: { initialValue: 10 }, key: "my-counter" }Keys work the same way as React's key prop — when the key stays the same, the resource keeps its state. When the key changes, the resource is unmounted and a fresh one is created.
Instances
A ResourceElement is just a description — it doesn't do anything on its own. To bring it to life, you create an instance. There are two ways to do this:
useResource
Use useResource inside React components. The resource's lifecycle is tied to the component — it mounts when the component mounts and unmounts when the component unmounts.
import { useResource } from "@assistant-ui/tap/react";
function CounterComponent() {
const { count, increment } = useResource(Counter({ initialValue: 10 }));
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}The component re-renders whenever the resource's state changes.
createResourceRoot
Use createResourceRoot for imperative, framework-agnostic usage.
import { createResourceRoot } from "@assistant-ui/tap";
const root = createResourceRoot();
const handle = root.render(Counter({ initialValue: 10 }));
// read state
handle.getValue().count; // 10
// subscribe to changes
handle.subscribe(() => {
console.log(handle.getValue().count);
});
// call methods
handle.getValue().increment();
// update props
root.render(Counter({ initialValue: 20 }));
// cleanup
root.unmount();