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

From MobX

A concept-by-concept map from MobX 6 to Retree — observables, observer(), computed, actions, and reactions — with a before/after store and the gotchas.

MobX and Retree share the same core bet: mutate objects, re-render exactly what read them. If you're comfortable in MobX, the model transfers almost directly — what changes is the wiring. This page is the routing table, not a tutorial; the Quickstart and Thinking in Retree cover the destination properly, and Retree vs MobX has the verified side-by-side comparison.

Concept map#

MobX 6Retree
makeAutoObservable(this) / observableNothing — pass the instance or object to Retree.root, or nest it in the tree
observer(Component)A hook per node the component reads: useNode, useTree, useSelect
action / runInActionNot needed — writes are plain assignment from anywhere
Batching writesRetree.runTransaction (opt-in, not required)
computed@memo getter for caching, @select getter for notification — MobX's one concept splits in two
autorun / reactionRetree.select (selector + callback) or Retree.on
toJS(observable)Retree.raw(node)
mobx-state-treeTree semantics are built in: Retree.parent, move, clone, link

Before and after#

A store class with derived state and an async method — the shape most MobX apps are made of:

MobX 6
import { makeAutoObservable, runInAction } from "mobx";
import { observer } from "mobx-react-lite";

class TaskStore {
    tasks: { text: string; done: boolean }[] = [];

    constructor() {
        makeAutoObservable(this);
    }

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

    async load() {
        const tasks = await fetchTasks();
        runInAction(() => {
            this.tasks = tasks;
        });
    }
}

const store = new TaskStore();

// Every reading component must be wrapped:
const DoneBadge = observer(function DoneBadge() {
    return <span>{store.doneCount}</span>;
});
Retree
import { Retree, ReactiveNode, memo, select } from "@retreejs/core";
import { useNode } from "@retreejs/react";

class TaskStore extends ReactiveNode {
    tasks: { text: string; done: boolean }[] = [];

    @select // the owner emits only when the count changes
    get doneCount() {
        return this.tasks.filter((task) => task.done).length;
    }

    async load() {
        this.tasks = await fetchTasks(); // no runInAction
    }

    get dependencies() {
        return [];
    }
}

const store = Retree.root(new TaskStore());

// No wrapper — the hook is the subscription:
function DoneBadge() {
    const state = useNode(store);
    return <span>{state.doneCount}</span>;
}

A plain class (no ReactiveNode) also works when you don't need derived-value notification — useNode plus methods that mutate this covers most stores.

Gotchas#

  • Granularity moves from reads to hooks. observer() tracks whatever the render read; in Retree the hook call defines the subscription, and useNode(node) covers that node's own fields only. Give each rendered child node its own useNode — the rule is spelled out in Common pitfalls. The upside: there is no silently-non-reactive component, MobX's classic missing-observer failure.
  • One structural parent per node. MobX lets one observable sit in two stores; Retree throws on aliasing. Say what you mean with link, move, or clone.
  • Decorators are optional and standard. Retree's @memo / @select are 2023-11 standard decorators, not MobX's legacy flavor — if your tsconfig still has experimentalDecorators: true for MobX, see Setup & decorators. Every decorator has a non-decorator equivalent.
  • No enforceActions. Nothing stops a write from anywhere, and every write emits immediately. Batch multi-write updates with Retree.runTransaction where MobX's action batched for you.

Where next#

  • Retree vs MobX — the verified feature-by-feature table.
  • ReactiveNode & decorators — the full view-model toolkit computed users will want.
  • Select semantics — where MobX's one computed splits into caching vs notification.
← PreviousCompatibilityNext →From Zustand

On this page

  • Concept map
  • Before and after
  • Gotchas
  • Where next