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

Select semantics

The canonical reference for Retree's three selects — useSelect and Retree.select are observational, @select makes the owning node emit — and when to reach for each.

Three Retree APIs share the name "select": useSelect in React, Retree.select in core, and the @select decorator on ReactiveNode getters. They share the name because they share a job — react to a selection instead of a whole node — but they split into two behaviors, and mixing them up is the most common source of "why didn't that update?" questions. This page is the reference for the distinction; every other mention of these APIs links back here.

The one distinction#

useSelect and Retree.select are observational: when a selected dependency changes, your subscriber is notified — the component re-renders, the callback runs — but the node you passed in is not touched. It does not emit nodeChanged, and it does not receive a fresh reproxy, so other subscribers and memo comparisons of that node see no change.

@select is emitting: when the getter's selected dependencies change, the owning ReactiveNode itself emits nodeChanged and reproxies. Every useNode(owner) subscriber re-renders, and the owner's identity changes for React.memo and dependency arrays.

APIWhere it runsWhen the selection changesObserved node reproxied?
useSelectA React componentThat component re-renders❌ observational
Retree.selectAnywhere — no React neededYour callback runs❌ observational
@selectA ReactiveNode getterThe owner emits nodeChanged and reproxies — every subscriber updates✅ emitting

Observational: useSelect and Retree.select#

The subscription belongs to the caller, and the notification stops there. In the example below the selection changes, so the callback runs — but the tasks array never emits nodeChanged, because none of its own fields changed:

import { Retree } from "@retreejs/core";

const project = Retree.root({
    tasks: [
        { title: "Docs", done: false },
        { title: "Tests", done: true },
    ],
});

Retree.on(project.tasks, "nodeChanged", () => console.log("tasks emitted"));

Retree.select(
    project.tasks,
    (tasks) => tasks.filter((task) => task.done).length,
    (doneCount) => console.log("done count:", doneCount),
    { listenerType: "treeChanged" }
);

project.tasks[0].done = true;
// ✅ "done count: 2" — the selection changed, the callback ran
// ❌ no "tasks emitted" — the observed node did not emit or reproxy

useSelect is the same primitive with a re-render instead of a callback. That privacy is the point: a header can watch a done-count across the whole tree without making any other component — or any memo comparison — believe the tree changed.

Emitting: @select#

Put the selection on the node when the derived value belongs to the node and everyone subscribed to it should see updates. @select traps the getter's reads (or takes an explicit selector), and when the selection changes, the owner emits like one of its own fields changed:

import { Retree, ReactiveNode, select } from "@retreejs/core";

class TaskBoard extends ReactiveNode {
    public tasks = [
        { title: "Docs", done: false },
        { title: "Tests", done: true },
    ];

    @select
    get doneCount() {
        return this.tasks.filter((task) => task.done).length;
    }

    get dependencies() {
        return [];
    }
}

const board = Retree.root(new TaskBoard());
Retree.on(board, "nodeChanged", () => console.log("board emitted"));

board.tasks[0].done = true; // ✅ "board emitted" — doneCount changed
board.tasks[0].title = "Better docs"; // ❌ silent — the selection stayed 2

Because the owner reproxies, plain useNode(board) is enough in components — no useSelect needed at the call sites. The full decorator forms (bare, explicit selector, equals option) are covered in ReactiveNode & decorators.

Which one do I want#

  • One component narrowing its own re-renders — useSelect. The selection matters to that component only.
  • A callback outside React — Retree.select. Services, tests, integrations.
  • A derived value that belongs to the node — @select. Every useNode(owner) subscriber, memo comparison, and dependency array should see the owner change when the selection does.

A useful smell: if several components call useSelect with the same selector over the same node, that selection probably wants to be a @select getter on the node instead.

None of them cache#

All three selects narrow notification — none of them cache the selector's result for reuse. Selectors can run on candidate changes just to decide whether the selection changed. Put expensive computation behind memo and select the cached value; memoization caches, selection narrows, and neither substitutes for the other. See the matching pitfall.

Where next#

  • useSelect — the React hook's three forms and a live demo.
  • Events & subscriptions — Retree.select alongside Retree.on.
  • ReactiveNode & decorators — @select forms and the memo family.
← PreviousSetup & decoratorsNext →Performance

On this page

  • The one distinction
  • Observational: useSelect and Retree.select
  • Emitting: @select
  • Which one do I want
  • None of them cache
  • Where next