Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

117 lines
3.9 KiB
Plaintext

---
title: Composition
description: 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.
<Callout type="info">
**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.
</Callout>
## useResource
Render a single child resource. The child has its own state and effects, and is automatically cleaned up when the parent unmounts.
```ts
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](https://react.dev/learn/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.
```ts
const value = useResource(Counter({ incrementBy }));
```
## useResources
<Callout type="warn">
This API is experimental and may change.
</Callout>
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.
```ts
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](/tap/docs/tap/trees-and-rerenders) 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.
```ts
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 |