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
117 lines
3.9 KiB
Plaintext
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 |
|