How tap differs from React

The handful of behaviors that aren't quite React.

Resources are React hooks, so almost everything carries over: the rules of hooks, dependency arrays, memoization, refs, and effect cleanup all work as you expect. This page covers the few places tap deliberately differs.

Effects run in call order

In React, effects run children-first (inside-out), because a component can only reach its children by returning them. In tap, useResource is just another hook, so effects run in the exact order they are called, and you can place effects before or after a child.

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

const useParent = () => {
  useEffect(() => console.log("1: before child"));
  const child = useResource(Child());
  useEffect(() => console.log("3: after child"));
  return child;
};

const Parent = resource(useParent);

const useChild = () => {
  useEffect(() => console.log("2: child"));
};

const Child = resource(useChild);
// Mount order: 1, 2, 3

Cleanup on unmount runs in the same order (FIFO). This lets a parent run setup both before and after its children, which is useful when it reacts to data a child provides.

The tree re-renders from the root

This is the biggest difference. In React, a state change re-renders only the component that changed and its descendants. In tap, the whole resource tree re-renders from the root: because a parent reads its child's return value directly, the parent must re-run to receive the new value, and so must its parent, all the way up.

useTapRoot breaks this chain. It creates a subtree boundary: everything below it re-renders independently, and the parent does not re-render when the subtree updates. This is how you keep a large tree efficient and how store libraries are built on tap.

useLayoutEffect collapses onto useEffect

tap has a single effect primitive. useLayoutEffect is accepted (so React code ports cleanly) but behaves like useEffect inside a resource.