Documents
composition
composition
Type
External
Status
Published
Created
Mar 17, 2026
Updated
Mar 17, 2026

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, `tapResource` returns the child's value directly, making composition a tool for building up state and logic, not just trees.

tapResource#

Render a single child resource. The child has its own state and effects, and is automatically cleaned up when the parent unmounts.

const Timer = resource(() => {
  const counter = tapResource(Counter());

  tapEffect(() => {
    const interval = setInterval(() => {
      counter.increment();
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return { count: counter.count };
});

Props dependencies#

This API is experimental and may change.

By default, the child re-renders whenever the parent re-renders. You can pass a dependency array to control when the child receives new props.

const result = tapResource(Counter({ incrementBy }), [incrementBy]);

tapResources#

This API is experimental and may change.

Render a dynamic list of child resources. Each element must have a key via withKey. Resources are preserved across renders when their key stays the same.

import { withKey } from "@assistant-ui/tap";

const TodoList = resource(() => {
  const [items, setItems] = tapState([
    { id: "1", text: "Learn tap" },
    { id: "2", text: "Build something" },
  ]);

  const todos = tapResources(
    () => items.map((item) => withKey(item.id, TodoItem({ text: item.text }))),
    [items],
  );

  return {
    todos,
    add: (text: string) =>
      setItems((prev) => [...prev, { id: crypto.randomUUID(), text }]),
  };
});
Every element passed to `tapResources` must have a key. Keys must be unique within the list. Missing or duplicate keys will throw an error.

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.

tapResourceRoot#

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

const counter = tapResourceRoot(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#

HookUse when
tapResourceYou need an independent child with its own state and lifecycle
tapResourcesYou have a dynamic list of children
tapResourceRootYou need to expose a child as a subscribable store