Compare
Valtio and Retree are the two most similar libraries on this site: both wrap your state in proxies, both let you mutate it directly, and both re-render only the components that read what changed. Everything below was verified against Valtio 2.3 in July 2026; if we got something wrong, submit a PR.
Start where the two diverge most: a deeply nested tree. Both versions below re-render exactly one row per write — the difference is how many objects each component has to hold to get there.
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 }) {
// One object: read it, write it, subscribe to it.
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.import { proxy, useSnapshot } from "valtio";
const board = proxy({
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 }) {
// Two objects per row: read the frozen snap,
// write the mutable proxy.
const snap = useSnapshot(item);
return (
<label>
<input
type="checkbox"
checked={snap.done}
onChange={() => (item.done = !item.done)}
/>
{snap.text}
</label>
);
}
board.columns[2].cards[0].checklist[1].done = true;
// Access tracking keeps this narrow too — the cost is
// the state/snap split in every component, and memo'd
// children handed un-accessed snapshots can over-render.That split is the defining difference. Valtio keeps state in two objects: you mutate the proxy and render from the frozen snap that useSnapshotreturns. Valtio's own docs list the gotchas that follow: React.memo children can over-render when handed snapshot objects they never accessed, controlled inputs need sync: true, and object getters are uncached and resolve against siblings only. Retree gives you one object — the same reference is read in render and mutated in handlers, and the subscription comes from which node you pass to useNode.
Even the simplest case shows it. The same counter in both:
import { Retree } from "@retreejs/core";
import { useNode } from "@retreejs/react";
const counter = Retree.root({ count: 0 });
function Counter() {
// One object: read and write the same reference.
const state = useNode(counter);
return (
<button onClick={() => (state.count += 1)}>
Count: {state.count}
</button>
);
}import { proxy, useSnapshot } from "valtio";
const state = proxy({ count: 0 });
function Counter() {
// Two objects: read from the frozen snap,
// write to the mutable state proxy.
const snap = useSnapshot(state);
return (
<button onClick={() => (state.count += 1)}>
Count: {snap.count}
</button>
);
}Granularity works differently too. Valtio tracks which snapshot fields a component accessed and re-renders when those change. Retree makes granularity structural and explicit: useNode(task)subscribes to that node's own fields, useTree widens to a subtree, useSelect narrows to a projection, and useRaw gives proxy-free reads for hot paths. Explicit means the granularity is visible in the code — you can read which node a component subscribes to instead of inferring it from access patterns. It also means picking the node is a real decision; common pitfalls covers every known trap.
Trees are Retree's home turf. Valtio has no tree semantics — no structural parent, no move/clone/link. Retree builds them into core: Retree.parent, Retree.move, Retree.clone, Retree.link. It also ships a first-party backend integration (@retreejs/convex, for Convex); no official backend integration exists for Valtio.
Now the other side of the ledger, stated plainly. Valtio is 2.8 kB min+gzip in our measurements; Retree core + react is 18.1 kB. What those bytes buy: per-node subscriptions, tree operations, view models with optional decorators, transactions, and the Convex integration — the things you would otherwise build yourself on top of a minimal proxy. Valtio also supports Redux DevTools, which Retree does not yet, and it has production mileage where Retree is v0.5.x. The trade-offs section on /why covers the rest.
The summary: Valtio optimizes for the smallest possible proxy-state core. Retree optimizes for state shaped like your component tree — nested nodes, per-node subscriptions, parent / move / clone / link — with one object instead of a state/snapshot pair. If your state is a tree, that is the problem Retree was built to win.
| Feature | Retree@retreejs/core + react 0.5.1 | Valtio2.3 |
|---|---|---|
| Mutation model | First-class:Plain assignment on plain objects and classes | First-class:Mutate proxy state; render from a frozen snap1 |
| Subscription granularity | First-class:Per node — you pick with useNode / useTree / useSelect / useRaw | First-class:Access-tracked snapshots1 |
| 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 orientation2 |
| Hooks without HOC wrappers | First-class:Hooks only | First-class:useSnapshot hook |
| Class instances as state | First-class:Classes and plain objects, no registration call | Not scored:Not verified3 |
| Optional decorators | First-class:Standard 2023-11 decorators: @select, @memo, @ignore, @link — always optional | Not scored:Not verified3 |
| Computed / memoized values | First-class:memo, @memo, @fnMemo, @select | Possible with work:Getters are uncached and siblings-only1 |
| Transactions & silent writes | First-class:Retree.runTransaction, Retree.runSilent | Not scored:Not verified3 |
| Tree operations: parent / move / clone / link | First-class:Built in: Retree.parent, move, clone, link | Absent:No tree semantics2 |
| Escape hatches for hot paths | First-class:Retree.raw, useRaw, peekInto, untracked | Not scored:Not verified3 |
| First-party backend integration | First-class:@retreejs/convex (Convex)4 | Absent:No official backend integration4 |
| DevtoolsRetree trails here | Absent:None today5 | First-class:Redux DevTools |
| Concurrent React (useSyncExternalStore) | Possible with work:useState + useEffect subscriptions; no useSyncExternalStore yet6 | Not scored:Not verified3 |
| TypeScript inference | First-class:Hooks return your object's own type | Not scored:Not verified3 |
| Bundle size (min+gzip, measured by us) | Measured value:18.1 kB (core + react)7 | Measured value:2.8 kB7 |
| Ecosystem & community resourcesRetree trails here | Absent:Young — this site and the repo are the resources today8 | First-class:Established |
| Production track recordRetree trails here | Absent:v0.5.x — no known large deployments8 | First-class:Established |
| Framework-agnostic usage beyond ReactRetree trails here | Possible with work:Core runs anywhere; React is the only first-class binding9 | First-class:Yes |
| Redux DevTools supportRetree trails here | Absent:No5 | First-class:Yes |
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.