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