Nest and compose resources with useResource, useResources, and useTapRoot.
Resources can render other resources. Child resources get their own fiber, lifecycle, and state, just like React components rendering other components.
Key difference from React: parent resources can directly access the return values of their children. In React, a parent component never sees what its children render. In tap, useResource returns the child's value directly, making composition a tool for building up state and logic, not just trees.
useResource
Render a single child resource. The child has its own state and effects, and is automatically cleaned up when the parent unmounts.
import { resource, useResource } from "@assistant-ui/tap";
import { useEffect } from "react";
const useTimer = () => {
const counter = useResource(Counter());
useEffect(() => {
const interval = setInterval(() => {
counter.increment();
}, 1000);
return () => clearInterval(interval);
}, []);
return { count: counter.count };
};
const Timer = resource(useTimer);Controlling re-renders
The child re-renders when the element identity changes. The React Compiler keeps the element stable across renders when its props are unchanged, so the child only re-renders when its inputs actually change, no manual dependency tracking required.
const value = useResource(Counter({ incrementBy }));useResources
This API is experimental and may change.
Render a dynamic list of child resources, passed as an array. Each element must have a key via withKey. Resources are preserved across renders when their key stays the same.
import { resource, withKey, useResources } from "@assistant-ui/tap";
import { useState } from "react";
const useTodoList = () => {
const [items, setItems] = useState([
{ id: "1", text: "Learn tap" },
{ id: "2", text: "Build something" },
]);
const todos = useResources(
items.map((item) => withKey(item.id, TodoItem({ text: item.text }))),
);
return {
todos,
add: (text: string) =>
setItems((prev) => [...prev, { id: crypto.randomUUID(), text }]),
};
};
const TodoList = resource(useTodoList);Keys must be unique; a missing or duplicate key throws.
Key behavior
- Same key, same type: the existing fiber is reused and re-rendered with new props
- Same key, different type: the old fiber is unmounted and a new one is created
- Removed key: the fiber is unmounted and cleaned up
This is the same model as React's key prop on list elements.
useTapRoot
useTapRoot takes a render callback and returns a stable { getValue, subscribe } handle instead of the child's value directly. The parent doesn't re-render when the child updates, consumers subscribe to changes instead. See Trees & Re-renders for why this matters.
Pass a named function (function CounterRoot() { ... }), not an arrow. The body calls hooks, so the name is what lets React's rules-of-hooks lint it.
import { useResource, useTapRoot } from "@assistant-ui/tap";
const counter = useTapRoot(function CounterRoot() {
return useResource(Counter());
});
// read current value
counter.getValue(); // { count: 0, increment: ... }
// subscribe to changes
const unsub = counter.subscribe(() => {
console.log(counter.getValue().count);
});The returned object has a stable identity and won't change across renders. This is the pattern used to build store libraries on top of tap.
When to use which
| Hook | Use when |
|---|---|
useResource | You need an independent child with its own state and lifecycle |
useResources | You have a dynamic list of children |
useTapRoot | You need to expose a child as a subscribable store |