retree
DocsAPIWhy Retree

Start here

  • Quickstart
  • Thinking in Retree
  • Common pitfalls

React

  • Choosing a hook
  • useRoot
  • useNode
  • useTree
  • useSelect
  • useRaw

Core

  • Events & subscriptions
  • Effects & reactions
  • Tree operations
  • Transactions & silent writes
  • Undo & redo

View models

  • ReactiveNode & decorators
  • Setup & decorators

Going deeper

  • Select semantics
  • Performance
  • React Compiler
  • Testing
  • DevTools
  • Convex integration
  • Async queries
  • Compatibility

Migrate

  • From MobX
  • From Zustand
  • From Redux Toolkit

Start here

  • Quickstart
  • Thinking in Retree
  • Common pitfalls

React

  • Choosing a hook
  • useRoot
  • useNode
  • useTree
  • useSelect
  • useRaw

Core

  • Events & subscriptions
  • Effects & reactions
  • Tree operations
  • Transactions & silent writes
  • Undo & redo

View models

  • ReactiveNode & decorators
  • Setup & decorators

Going deeper

  • Select semantics
  • Performance
  • React Compiler
  • Testing
  • DevTools
  • Convex integration
  • Async queries
  • Compatibility

Migrate

  • From MobX
  • From Zustand
  • From Redux Toolkit
Loading page…

retree

Reactive object trees for React. MIT licensed.

© 2026 Ryan Bliss

Docs

  • Quickstart
  • Thinking in Retree
  • React hooks
  • Common pitfalls

Reference

  • @retreejs/core
  • @retreejs/query
  • @retreejs/react
  • @retreejs/devtools
  • @retreejs/convex
  • @retreejs/react-convex

Project

  • Why Retree
  • GitHub
  • npm
  • llms.txt

Docs

Edit on GitHub

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#

HookRe-renders whenUse it forAvoid when
useNodedirect nodeChanged events on one node — its own fields changerows, panels, forms, and focused child componentsthe component only needs a derived value — reach for useSelect
useTreetreeChanged events from a node or any descendantsmall subtrees that truly need broad invalidationhot paths and large app-level roots
useSelecta selected value or ordered dependency list changescounts, totals, booleans, and other narrow projections — including ones computed across a deep subtreeyou want to cache expensive computation — it is a subscription primitive, not a memo cache
useRawsame events as useNode (nodeChanged by default), but you read rawcomponents that read wide during render: big tables, canvas layers, serializationchildren need node props and you skip toManaged, or you write to raw values
useRootnever by itself — it creates a root; pair it with a subscribing hookstate that belongs to one React subtree, for that component's lifetimestate 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.

useNode(project.tasks)renders: 1

done: 1/2 (computed here — goes stale on toggle; that's a job for useSelect)

  • renders: 1
  • renders: 1

Subscription boundary

project
tasks
tasks[0]
tasks[1]

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 (and treeChanged in general) as broad subtree invalidation, especially in hot paths.
  • Use useRaw when the render body itself reads wide — the subscription stays as narrow as useNode, 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 like useNode, read proxy-free.

Full signatures live in the API reference.

← PreviousCommon pitfallsNext →useRoot

On this page

  • The decision table
  • Try it
  • The narrowest subscription wins
  • Per-request roots with RetreeProvider
  • Testing components
  • The five hooks