Docs
Choosing a hook
useNode, useTree, useSelect, useRaw, and useRoot subscribe to the same tree with different granularity. Pick the narrowest one that answers "when should this component re-render?"
Every Retree hook answers the same question — when should this component re-render? — at a different granularity. They all read the same tree; the only thing you are choosing is the subscription.
The decision table#
| Hook | Re-renders when | Use it for | Avoid when |
|---|---|---|---|
useNode | direct nodeChanged events on one node — its own fields change | rows, panels, forms, and focused child components | the component only needs a derived value — reach for useSelect |
useTree | treeChanged events from a node or any descendant | small subtrees that truly need broad invalidation | hot paths and large app-level roots |
useSelect | a selected value or ordered dependency list changes | counts, totals, booleans, and other narrow projections — including ones computed across a deep subtree | you want to cache expensive computation — it is a subscription primitive, not a memo cache |
useRaw | same events as useNode (nodeChanged by default), but you read raw | components that read wide during render: big tables, canvas layers, serialization | children need node props and you skip toManaged, or you write to raw values |
useRoot | never by itself — it creates a root; pair it with a subscribing hook | state that belongs to one React subtree, for that component's lifetime | state that should outlive the component — create a module-scope Retree.root instead |
Try it#
The playground below runs one shared tree — project.tasks — through each
hook. The render counters are real: switch modes, press the same mutation
buttons, and compare which components re-render.
useNode: the panel subscribes to the tasks array, each row to its own task — toggle and rename re-render one row; add re-renders the panel.
done: 1/2 (computed here — goes stale on toggle; that's a job for useSelect)
- renders: 1
- renders: 1
Subscription boundary
boundary: nodeChanged subscription on tasks, plus one per row task
The narrowest subscription wins#
Retree performs best when components subscribe to the narrowest node or value they need:
- Prefer
useNode(child)for item rows and focused panels. - Prefer
useSelect(node, selector)for selected values or dependency lists that should only re-render when the selection changes. - Treat
useTree(andtreeChangedin general) as broad subtree invalidation, especially in hot paths. - Use
useRawwhen the render body itself reads wide — the subscription stays as narrow asuseNode, but reads skip the proxy. - Avoid constructing Retree roots during render; create them at module scope
or with
useRoot.
When two hooks would both work, pick the one further up this list. The subscription boundaries should follow your component boundaries — that idea is developed in Thinking in Retree, and the first-hour mistakes are catalogued in Common pitfalls.
Per-request roots with RetreeProvider#
Module-singleton roots (const app = Retree.root(...) at module scope) are
shared by everything that imports the module. In a client-only app that is
often exactly right — but in SSR frameworks (Next.js, Remix) module state on
the server is shared across requests, so one user's writes can leak into
another user's render. It also makes tests share state.
RetreeProvider solves both: its
create factory runs exactly once per mounted provider (surviving Strict
Mode), so each server request — and each test render — gets its own container
of roots. Descendants read the container with
useRootContext:
import { Retree } from "@retreejs/core";
import { RetreeProvider, useRootContext, useNode } from "@retreejs/react";
const createRoots = () => ({ counter: Retree.root({ count: 0 }) });
function Counter() {
const { counter } = useRootContext<ReturnType<typeof createRoots>>();
const state = useNode(counter);
return <button onClick={() => (state.count += 1)}>{state.count}</button>;
}
export function App() {
return (
<RetreeProvider create={createRoots}>
<Counter />
</RetreeProvider>
);
}Prefer createRetreeContext when
you want the container type to flow with no type arguments — the T you
create the context with is the T every useRootContext() returns:
import { createRetreeContext } from "@retreejs/react";
const { Provider: AppProvider, useRootContext: useAppRoots } =
createRetreeContext<ReturnType<typeof createRoots>>("AppProvider");The provider is a client component, so a server component can render it, but
the create factory runs in the client/SSR render pass — not in React Server
Components. Apps with one lifetime-of-the-page tree don't need a provider at
all; module-scope roots and useRoot stay the
simplest paths.
Testing components#
@retreejs/react/testing ships two helpers for component tests:
createTestRoot(factory) returns a managed root plus a cleanup() that
clears every listener the test leaked, and actOnRetree(write) wraps Retree
writes in React's act so renders flush before your assertions. The
Testing guide shows them with Testing Library.
The five hooks#
useRoot— create a root for a component lifetime.useNode— subscribe to one node's own fields.useTree— subscribe to a node and all descendants.useSelect— subscribe to a selected value.useRaw— subscribe likeuseNode, read proxy-free.
Full signatures live in the API reference.