Compare
If you like MobX's mutable model but are looking for an alternative without observer() wrappers, this page is for you. Retree and MobX solve the same problem — mutate objects, re-render only what read them — with different contracts. Everything below was verified against MobX 6.16 with mobx-react-lite 4.1 in July 2026; if we got something wrong, submit a PR.
Start with the case Retree is built for: a write three levels deep in a nested tree, and exactly one row re-rendering. Both versions below achieve fine-grained updates — the difference is what each one demands from every component along the way. The MobX version uses its current, documented APIs — makeAutoObservable in the constructor (which also marks methods as actions) and observer() from mobx-react-lite — not the legacy decorator style.
import { Retree } from "@retreejs/core";
import { useNode } from "@retreejs/react";
const board = Retree.root({
columns: [
{
title: "In progress",
cards: [
{
title: "Ship v1",
checklist: [
{ text: "Write docs", done: false },
{ text: "Cut release", done: false },
],
},
],
},
// ...more columns
],
});
type ChecklistItem = { text: string; done: boolean };
function ChecklistRow({ item }: { item: ChecklistItem }) {
// Subscribe to this row's node — nothing else.
const row = useNode(item);
return (
<label>
<input
type="checkbox"
checked={row.done}
onChange={() => (row.done = !row.done)}
/>
{row.text}
</label>
);
}
// Three levels deep, plain assignment, from anywhere:
board.columns[2].cards[0].checklist[1].done = true;
// Exactly one ChecklistRow re-renders. No observer(),
// no action, no memoization pass.import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class BoardStore {
columns = [
{
title: "In progress",
cards: [
{
title: "Ship v1",
checklist: [
{ text: "Write docs", done: false },
{ text: "Cut release", done: false },
],
},
],
},
// ...more columns
];
constructor() {
makeAutoObservable(this);
}
toggle(item: { done: boolean }) {
// Writes go through an action —
// enforceActions is on by default.
item.done = !item.done;
}
}
const board = new BoardStore();
type ChecklistItem = { text: string; done: boolean };
// Fine-grained tracking works here too — as long as
// every component reading the board is wrapped:
const ChecklistRow = observer(function ChecklistRow({
item,
}: {
item: ChecklistItem;
}) {
return (
<label>
<input
type="checkbox"
checked={item.done}
onChange={() => board.toggle(item)}
/>
{item.text}
</label>
);
});
// Miss observer() on one child and that child
// silently stops updating.That is the central difference in one example. MobX 6 requires observer() around every component that reads observables, and writes are expected to go through action — enforceActions is on by default. The sharp edge is what happens when you forget: an observable passed into a non-observer child silently stops being reactive. Retree has no equivalent failure mode because there is no wrapper — components subscribe with hooks (useNode, useTree, useSelect, useRaw), and writes are plain assignment with no action requirement.
The simplest case shows the same contract with less ceremony on both sides. Here is a counter in each:
import { Retree } from "@retreejs/core";
import { useNode } from "@retreejs/react";
const counter = Retree.root({ count: 0 });
function Counter() {
const state = useNode(counter);
return (
<button onClick={() => (state.count += 1)}>
Count: {state.count}
</button>
);
}import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count += 1;
}
}
const counter = new CounterStore();
const Counter = observer(function Counter() {
return (
<button onClick={() => counter.increment()}>
Count: {counter.count}
</button>
);
});Class support is a strength both share: MobX with makeAutoObservable(this), Retree with no registration call at all — pass a class instance to Retree.root and its methods and fields just work. Retree also layers optional standard 2023-11 decorators (@select, @memo, @ignore, @link) on top for view models that want them.
Trees are where the two diverge most. MobX core has no tree semantics — no notion of a node's parent, no move/clone/link — and defers that territory to mobx-state-tree, a separate schema-based layer. In Retree, tree operations are built into core: Retree.parent, Retree.move, Retree.clone, and Retree.link work on your plain objects directly.
What MobX brings that Retree doesn't: a decade of production hardening, a best-in-class computed engine, a large ecosystem, and framework-agnostic usage well beyond React. Retree is v0.5.x — what it offers against that maturity is a smaller surface you can audit directly, with the test suite and benchmark harness open in the repo. The full trade-offs are on /why, stated plainly.
Bundle size mildly favors Retree: in our measurements (esbuild, min+gzip, react externalized, July 2026), Retree core + react is 18.1 kB and mobx + mobx-react-lite is 21.5 kB — with @retreejs/core carrying zero runtime dependencies.
| Feature | Retree@retreejs/core + react 0.5.1 | MobX6.16 + mobx-react-lite 4.1 |
|---|---|---|
| Mutation model | First-class:Plain assignment on plain objects and classes | First-class:Mutable observables; writes wrapped in action1 |
| Subscription granularity | First-class:Per node — you pick with useNode / useTree / useSelect / useRaw | First-class:Reads tracked inside observer()2 |
| Nested stores that match the component tree | First-class:The core idea — subscribe to any node in one tree | Possible with work:Top-level store orientation3 |
| Hooks without HOC wrappers | First-class:Hooks only | Absent:observer() required on every reading component2 |
| Class instances as state | First-class:Classes and plain objects, no registration call | First-class:makeAutoObservable(this) |
| Optional decorators | First-class:Standard 2023-11 decorators: @select, @memo, @ignore, @link — always optional | Not scored:Not verified4 |
| Computed / memoized values | First-class:memo, @memo, @fnMemo, @select | First-class:Best-in-class computed engine5 |
| Transactions & silent writes | First-class:Retree.runTransaction, Retree.runSilent | Not scored:Not verified4 |
| Tree operations: parent / move / clone / link | First-class:Built in: Retree.parent, move, clone, link | Absent:Not in core — defers to mobx-state-tree3 |
| Escape hatches for hot paths | First-class:Retree.raw, useRaw, peekInto, untracked | Not scored:Not verified4 |
| First-party backend integration | First-class:@retreejs/convex (Convex)6 | Absent:No official backend integration6 |
| DevtoolsRetree trails here | Absent:None today7 | Not scored:Not verified4 |
| Concurrent React (useSyncExternalStore) | Possible with work:useState + useEffect subscriptions; no useSyncExternalStore yet8 | Not scored:Not verified4 |
| TypeScript inference | First-class:Hooks return your object's own type | Not scored:Not verified4 |
| Bundle size (min+gzip, measured by us) | Measured value:18.1 kB (core + react)9 | Measured value:21.5 kB (mobx + mobx-react-lite)9 |
| Ecosystem & community resourcesRetree trails here | Absent:Young — this site and the repo are the resources today10 | First-class:Established |
| Production track recordRetree trails here | Absent:v0.5.x — no known large deployments10 | First-class:A decade of hardening5 |
| Framework-agnostic usage beyond ReactRetree trails here | Possible with work:Core runs anywhere; React is the only first-class binding11 | First-class:Yes |
| Redux DevTools supportRetree trails here | Absent:No7 | Not scored:Not verified4 |
Accuracy pledge
Comparison tables rot. Every claim above was verified against the listed versions in July 2026, and cells we could not verify are left unscored rather than guessed. Bundle sizes are our own measurements: esbuild, minified + gzip, react and react-dom externalized, July 2026, same method for every library. If anything here is wrong, stale, or unfair to another library, open an issue or submit a PR — corrections are merged gladly.
What about mobx-state-tree?
mobx-state-tree is the closest competitor to Retree on tree semantics, and it deserves the mention. It is a separate, schema-based layer over MobX: you define typed models up front and get a managed state tree in return. Retree aims at the same tree-shaped problems without the schema layer — nodes are your own plain objects and class instances, typed by TypeScript inference rather than a runtime model definition, and components subscribe through hooks instead of observer(). If you want runtime-enforced schemas for your tree, mobx-state-tree is the established choice; if you want the tree without the schemas, that is exactly the gap Retree exists to fill.