Resources

Define resources, pass props, and host them.

A resource is a self-contained unit of reactive state and logic: like a React component, but without UI.

Defining a resource

A resource body is a hook: a function that calls the React hooks you know, imported from "react". Write it as a use-prefixed hook, then pass it to resource() to turn it into a Resource.

import { resource } from "@assistant-ui/tap";
import { useState } from "react";

const useCounter = (props: { initialValue?: number }) => {
  const [count, setCount] = useState(props.initialValue ?? 0);

  return { count, increment: () => setCount((c) => c + 1) };
};

const Counter = resource(useCounter);

resource() turns a hook into a Resource, and useResource turns it back into a hook call. Keep the body in a use-prefixed binding: it gives the resource a stable identity for keys and devtools, and lets React's rules-of-hooks lint the body.

resource() returns a factory function. Calling the factory creates a ResourceElement, a lightweight description of what to render, not a live instance yet.

Resource elements

A ResourceElement is a plain { hook, args } object, the same idea as a React JSX element, but written as a function call instead of JSX.

const element = Counter({ initialValue: 10 });
// { hook: useCounter, args: [{ initialValue: 10 }] }

Calling the factory (Counter({ ... })) reads like the plain function call it is. Like a JSX element, the result is inert until you host it.

Props

Resources accept props like components do. When a resource re-renders with new props, hooks react to the change through their dependency arrays, exactly as in React.

Return value

Instead of JSX, a resource returns any JavaScript value: an object, array, or primitive. That return value is the resource's public API, and it is what callers read when they host it.

Hosting a resource

useResource hosts a single element, re-entering the Resource as a hook call. It works the same way in both worlds: inside a React component it ties the resource to the component's lifecycle, and inside another resource it nests the resource as a child.

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

function CounterButton() {
  const { count, increment } = useResource(Counter({ initialValue: 10 }));
  return <button onClick={increment}>Count: {count}</button>;
}

Unlike React, a parent reads its child's return value directly, which makes composition a tool for building up state and logic, not just trees.

import { resource, useResource } from "@assistant-ui/tap";

const useDashboard = () => {
  const counter = useResource(Counter({ initialValue: 0 }));
  return { total: counter.count };
};

const Dashboard = resource(useDashboard);

Next

  • Composition: render keyed lists with useResources and expose subscribable boundaries with useTapRoot.
  • Context: pass values through the resource tree without prop drilling.