API reference · generated from source
Class: Retree#
Defined in: Retree.ts:141
Main entry point for use with Retree package. Exposes utility functions for observing to changes to an object and its children.
Constructors#
Constructor#
new Retree(): Retree;Returns#
Retree
Methods#
clearListeners()#
static clearListeners(node, shallow?): void;Defined in: Retree.ts:958
Clear all listeners for a given node.
Parameters#
| Parameter | Type | Default value | Description |
|---|---|---|---|
node | object | undefined | node to clear all listeners for |
shallow | boolean | true | when false, also unsubscribes every descendant node — the full subtree, not just direct children. |
Returns#
void
Remarks#
Equivalent to calling each unsubscribe function returned by Retree.on.
Prefer storing and calling the unsubscribe returned from
Retree.on when you own a single subscription. Use
clearListeners when you own every listener for a node, such as during
teardown of a Retree-managed integration.
Example#
const root = Retree.root({ child: { grandchild: { count: 0 } } });
Retree.on(root, "nodeChanged", () => {});
Retree.on(root.child, "nodeChanged", () => {});
Retree.on(root.child.grandchild, "nodeChanged", () => {});
Retree.clearListeners(root, false); // clears all threeclone()#
static clone<TNode>(node): TNode;Defined in: Retree.ts:277
Clone a Retree-managed node into a detached object that can be assigned somewhere else as a new structural child.
Type Parameters#
| Type Parameter |
|---|
TNode extends object |
Parameters#
| Parameter | Type | Description |
|---|---|---|
node | TNode | Existing Retree-managed node to copy. |
Returns#
TNode
A detached copy of the node's current raw data.
Remarks#
Use this when two places need independent state initialized from the same current data. The clone is detached until you assign it into a Retree tree. Mutating the clone after assignment emits for the clone's new structural location, not the source node.
Do not use clone when the original object should simply move; use
Retree.move. Do not use clone for a selected-item pointer; use
Retree.link or @link.
Example#
const project = Retree.root({ tasks: [{ title: "Draft" }] });
const copy = Retree.clone(project.tasks[0]);
project.tasks.push(copy); // ✅ copy becomes a new child
project.tasks[1].title = "Published"; // source task is unchangedisNode()#
static isNode(value): value is object;Defined in: Retree.ts:652
Check whether a value is a Retree-managed node.
Parameters#
| Parameter | Type | Description |
|---|---|---|
value | unknown | Value to check; any type is accepted. |
Returns#
value is object
true when the value is a Retree-managed node.
Remarks#
Returns true only for values wrapped by a Retree proxy — objects
returned by Retree.root or read through an existing Retree
tree. Raw values are not managed nodes: this returns false for the
objects behind Retree.raw, for previous/new change payload
values, and for plain objects that were never rooted.
Use this as the guard in front of APIs that require a managed node
when a value may come from either side of the proxy boundary, such as
data that is sometimes Retree state and sometimes plain wire data:
Retree.isNode(value) ? Retree.raw(value) : value.
Example#
const project = Retree.root({ items: [{ score: 1 }] });
Retree.isNode(project); // true
Retree.isNode(project.items[0]); // true
Retree.isNode(Retree.raw(project)); // false — raw values are not managed
Retree.isNode({ score: 1 }); // false — never rootedlink()#
static link<TNode>(node): RetreeLink<TNode>;Defined in: Retree.ts:246
Create a reactive pointer to an existing Retree-managed node.
Type Parameters#
| Type Parameter |
|---|
TNode extends object |
Parameters#
| Parameter | Type | Description |
|---|---|---|
node | TNode | Existing Retree-managed node to point at. |
Returns#
RetreeLink<TNode>
A Retree-managed link object whose current points at node.
Remarks#
The returned link can be stored in a Retree tree without reparenting the
linked target. Replacing link.current emits for the link; mutating the
linked target emits from its structural location.
Use this for selected items, cross-references, and pointers into another part of the same tree. Do not use a link when ownership should move; use Retree.move instead. Do not use a link when the two locations should diverge independently; use Retree.clone instead.
Example#
const root = Retree.root({
tasks: [{ title: "Docs" }],
selected: null as RetreeLink<{ title: string }> | null,
});
root.selected = Retree.link(root.tasks[0]); // ✅ emits on root
root.selected.current.title = "Better docs"; // ✅ emits where task is owned
Retree.parent(root.selected.current) === root.tasks; // truemanaged()#
static managed<TNode>(value): TNode | undefined;Defined in: Retree.ts:739
Resolve a raw value back to its Retree-managed node.
Type Parameters#
| Type Parameter |
|---|
TNode extends object |
Parameters#
| Parameter | Type | Description |
|---|---|---|
value | TNode | Raw object to resolve. |
Returns#
TNode | undefined
The managed node, or undefined when none exists.
Remarks#
This is the inverse of Retree.raw for values that belong to a
Retree tree: given a raw object (for example an element read while
scanning a Retree.raw subtree, or a previous/new value from
a change payload), it returns the latest managed node — ready for
mutation, subscription, or navigation. Passing a managed node returns
its latest managed identity.
Returns undefined when the value has never been materialized as a
Retree node or is not part of a Retree tree; a miss is a normal query
outcome, not an error. Values become materialized when they are read
through a managed node, so scanning via managed proxies first (or using
useRaw's toManaged, which materializes direct children on demand)
guarantees resolution.
Example#
const project = Retree.root({ items: [{ score: 1 }] });
project.items.forEach(() => {}); // materialize
const rawItem = Retree.raw(project.items)[0];
const item = Retree.managed(rawItem);
if (item) item.score = 2; // ✅ emits normallymove()#
Call Signature#
static move<TNode, TValue>(
node,
destination,
key?): TNode;Defined in: Retree.ts:313
Move an existing Retree-managed node from its current parent to a new parent.
Type Parameters
| Type Parameter | Default type |
|---|---|
TNode extends object | - |
TValue extends object | TNode |
Parameters
| Parameter | Type | Description |
|---|---|---|
node | TNode extends TValue ? TNode : never | Existing Retree-managed node to move. |
destination | TValue[] | Retree-managed array, map, set, or object destination. |
key? | number | Insertion index for arrays, map key for maps, or property key for objects. |
Returns
TNode
The latest reproxy for the moved node.
Remarks
Retree is a pure tree: each node has one structural parent. Use move
when ownership should transfer from the old parent to the destination.
Retree finds the current parent with Retree.parent and removes
the node safely before inserting it into the destination.
Arrays accept an optional numeric insertion index. Maps and plain
objects require a key. Sets ignore the key. Do not manually delete the
node from its old parent before calling move.
Example
const workspace = Retree.root({
todo: [{ title: "Docs" }],
done: [] as { title: string }[],
});
const moved = Retree.move(workspace.todo[0], workspace.done);
workspace.todo.length; // 0
workspace.done[0] === moved; // trueCall Signature#
static move<TNode, TKey, TValue>(
node,
destination,
key): TNode;Defined in: Retree.ts:318
Move an existing Retree-managed node from its current parent to a new parent.
Type Parameters
| Type Parameter | Default type |
|---|---|
TNode extends object | - |
TKey | unknown |
TValue extends object | TNode |
Parameters
| Parameter | Type | Description |
|---|---|---|
node | TNode extends TValue ? TNode : never | Existing Retree-managed node to move. |
destination | Map<TKey, TValue> | Retree-managed array, map, set, or object destination. |
key | TKey | Insertion index for arrays, map key for maps, or property key for objects. |
Returns
TNode
The latest reproxy for the moved node.
Remarks
Retree is a pure tree: each node has one structural parent. Use move
when ownership should transfer from the old parent to the destination.
Retree finds the current parent with Retree.parent and removes
the node safely before inserting it into the destination.
Arrays accept an optional numeric insertion index. Maps and plain
objects require a key. Sets ignore the key. Do not manually delete the
node from its old parent before calling move.
Example
const workspace = Retree.root({
todo: [{ title: "Docs" }],
done: [] as { title: string }[],
});
const moved = Retree.move(workspace.todo[0], workspace.done);
workspace.todo.length; // 0
workspace.done[0] === moved; // trueCall Signature#
static move<TNode, TValue>(node, destination): TNode;Defined in: Retree.ts:327
Move an existing Retree-managed node from its current parent to a new parent.
Type Parameters
| Type Parameter | Default type |
|---|---|
TNode extends object | - |
TValue extends object | TNode |
Parameters
| Parameter | Type | Description |
|---|---|---|
node | TNode extends TValue ? TNode : never | Existing Retree-managed node to move. |
destination | Set<TValue> | Retree-managed array, map, set, or object destination. |
Returns
TNode
The latest reproxy for the moved node.
Remarks
Retree is a pure tree: each node has one structural parent. Use move
when ownership should transfer from the old parent to the destination.
Retree finds the current parent with Retree.parent and removes
the node safely before inserting it into the destination.
Arrays accept an optional numeric insertion index. Maps and plain
objects require a key. Sets ignore the key. Do not manually delete the
node from its old parent before calling move.
Example
const workspace = Retree.root({
todo: [{ title: "Docs" }],
done: [] as { title: string }[],
});
const moved = Retree.move(workspace.todo[0], workspace.done);
workspace.todo.length; // 0
workspace.done[0] === moved; // trueCall Signature#
static move<TNode, TDestination>(
node,
destination,
key): TNode;Defined in: Retree.ts:331
Move an existing Retree-managed node from its current parent to a new parent.
Type Parameters
| Type Parameter | Default type |
|---|---|
TNode extends object | - |
TDestination extends object | object |
Parameters
| Parameter | Type | Description |
|---|---|---|
node | TNode | Existing Retree-managed node to move. |
destination | TDestination | Retree-managed array, map, set, or object destination. |
key | RetreeObjectMoveKey<TDestination, TNode> | Insertion index for arrays, map key for maps, or property key for objects. |
Returns
TNode
The latest reproxy for the moved node.
Remarks
Retree is a pure tree: each node has one structural parent. Use move
when ownership should transfer from the old parent to the destination.
Retree finds the current parent with Retree.parent and removes
the node safely before inserting it into the destination.
Arrays accept an optional numeric insertion index. Maps and plain
objects require a key. Sets ignore the key. Do not manually delete the
node from its old parent before calling move.
Example
const workspace = Retree.root({
todo: [{ title: "Docs" }],
done: [] as { title: string }[],
});
const moved = Retree.move(workspace.todo[0], workspace.done);
workspace.todo.length; // 0
workspace.done[0] === moved; // trueon()#
static on<T, TEvent>(
node,
listenerType,
callback): () => void;Defined in: Retree.ts:398
Listen for changes to a node.
Type Parameters#
| Type Parameter | Default type |
|---|---|
T extends object | object |
TEvent extends TRetreeEvents | TRetreeEvents |
Parameters#
| Parameter | Type | Description |
|---|---|---|
node | T | the object to listen for changes to. |
listenerType | TEvent | the type of TRetreeEvents change events to listen to. |
callback | TEvent extends TRetreeChangedEvents ? (reproxiedNode, changes) => void : () => void | the callback function for your listener. |
Returns#
an unsubscribe function to clean up your listeners.
() => void
Remarks#
Use nodeChanged for changes directly owned by the node.
Use treeChanged for changes to the node or descendants.
Use nodeRemoved for when this node is removed from its parent.
Example#
// Create the root node
const counter = Retree.root({ count: 0 });
// Listen for changes to values of the node
const unsubscribe = Retree.on(counter, "nodeChanged", (reproxy) => {
console.log(reproxy !== counter); // output: false
console.log(reproxy.count === counter.count); // output: true
});
// Make a change
counter.count = counter.count + 1;
// Stop listening for changes
unsubscribe();parent()#
static parent(node): object | null;Defined in: Retree.ts:619
Get a parent node for a given child node, if it exists
Parameters#
| Parameter | Type | Description |
|---|---|---|
node | object | a child node to get the parent of |
Returns#
object | null
the parent node if it exists, otherwise null
Example#
const tree = Retree.root({
count: 0,
child: {
count: 0,
child: {
count: 0,
},
},
});
function recursiveLog(node) {
console.log(node.count);
// Get the parent of node, if it exists
const parent = Retree.parent(node);
if (!parent) return; // at top of tree
recursiveLog(parent);
}
Retree.on(tree.child.child, "nodeChanged", (child) => {
// Recursively log the count of this node and all its parents
recursiveLog(child);
});
tree.child.child.count = 1;peekInto()#
static peekInto<TNode, TResult>(node, fn): TResult;Defined in: Retree.ts:833
Run a read-only query against a node's raw object at native speed, then resolve the result back to its Retree-managed node when one exists.
Type Parameters#
| Type Parameter |
|---|
TNode extends object |
TResult |
Parameters#
| Parameter | Type | Description |
|---|---|---|
node | TNode | Retree-managed node to query. |
fn | (raw) => TResult | Read-only callback that receives the raw object behind node. |
Returns#
TResult
The callback result, resolved to its managed node when one exists.
Remarks#
peekInto combines Retree.raw and Retree.untracked:
the callback receives the raw object behind node, so every read
inside it skips proxy traps and dependency tracking. If the callback
returns an object that belongs to a Retree tree, the latest managed
node (reproxy, or base proxy when the node has never reproxied) is
returned instead, ready for mutation or subscription. Primitives,
null, undefined, and unmanaged objects are returned as-is.
Only the returned value itself is resolved. A container built inside
the callback — a filter result, a tuple, an object literal — is
returned unchanged with raw elements; resolve elements individually
when they must be managed. Children that have never been read through
the managed tree are not yet materialized and resolve to their raw
value; traverse the path once, or use prepareTree / autoPrepare,
when a managed result is required.
Example#
const project = Retree.root({
tasks: [
{ id: "a", done: false },
{ id: "b", done: true },
],
});
project.tasks.forEach(() => {}); // materialize once (or prepareTree)
const task = Retree.peekInto(project.tasks, (rawTasks) =>
rawTasks.find((candidate) => candidate.id === "b")
);
// `task` is the managed node: mutations emit normally.
if (task) task.done = false; // ✅ emits
const doneCount = Retree.peekInto(
project.tasks,
(rawTasks) => rawTasks.filter((candidate) => candidate.done).length
); // ✅ primitive result returned as-israw()#
static raw<TNode>(node): TNode;Defined in: Retree.ts:703
Get the raw, unproxied object behind a Retree-managed node for read-only, non-reactive access.
Type Parameters#
| Type Parameter |
|---|
TNode extends object |
Parameters#
| Parameter | Type | Description |
|---|---|---|
node | TNode | Retree-managed node to unwrap. |
Returns#
TNode
The raw object behind the node.
Remarks#
Reads through Retree proxies pay per-property trap overhead. That is usually irrelevant, but algorithms that scan large collections of deeply nested nodes can read the raw object at native speed instead.
Treat the returned object as read-only. Mutating it directly skips
Retree change emission and can desynchronize memoized comparisons; make
all writes through the managed node. Reads through raw are invisible
to reactivity: they are not trapped by useSelect/Retree.select
selectors or @memo dependency collection, and children read this way
are not prepared for Retree.parent / Retree.on usage.
Raw purity guarantee: the returned subtree contains zero Retree
proxies, under every write path — including reparenting assignments,
Retree.move, Map/Set value reads, and post-construction assignment of
class instances or collections. Every value is plain data, every read
is native speed, and structuredClone(Retree.raw(node)) is a valid
point-in-time copy. Use Retree.managed to resolve a raw value
back to its managed node.
Throws when the value is not a Retree-managed node. When a value may
come from either side of the proxy boundary, guard with
Retree.isNode: Retree.isNode(value) ? Retree.raw(value) : value.
Example#
const project = Retree.root({ items: [{ score: 1 }, { score: 92 }] });
const rawItems = Retree.raw(project.items);
const total = rawItems.reduce((sum, item) => sum + item.score, 0); // ✅ native-speed read
project.items[0].score = 50; // ✅ writes stay on the managed treeroot()#
static root<T>(object): T;Defined in: Retree.ts:214
Builds a Retree compatible root node for the root object of your tree.
Type Parameters#
| Type Parameter | Default type |
|---|---|
T extends object | object |
Parameters#
| Parameter | Type | Description |
|---|---|---|
object | T | a root TreeNode for your tree |
Returns#
T
a Retree compatible object of type T
Remarks#
Use this once where plain state enters Retree. The returned proxy is
compatible with Retree.on, Retree.parent,
Retree.move, Retree.link, and React hooks from
@retreejs/react.
Do mutate the returned tree directly with normal JavaScript assignment
and collection methods. Do not call Retree.root(...) on every child
you assign into the tree; Retree prepares children as they are attached
or read.
Example#
const counter = Retree.root({ count: 0 });
Retree.on(counter, "nodeChanged", () => console.log(counter.count));
counter.count = counter.count + 1;runSilent()#
static runSilent(transaction, skipReproxy?): void;Defined in: Retree.ts:882
Run a synchronous transaction that will not cause `Retree.on listeners to emit.
Parameters#
| Parameter | Type | Default value | Description |
|---|---|---|---|
transaction | () => void | undefined | transaction function to run |
skipReproxy | boolean | true | skip reproxying nodes such that subsequent comparisons are equal. defaults to true. |
Returns#
void
Remarks#
Use runSilent for non-rendered bookkeeping or integration state that
should update without notifying Retree listeners. By default it also
skips reproxying, so old and new object identities stay equal for later
comparison checks.
Pass skipReproxy = false when you want to suppress listener emission
but still refresh reproxy identities.
Example#
const state = Retree.root({ rendered: 0, telemetry: 0 });
Retree.on(state, "nodeChanged", () => console.log("render"));
Retree.runSilent(() => {
state.telemetry += 1;
}); // ❌ no listener emit
state.rendered += 1; // ✅ emitsrunTransaction()#
static runTransaction(transaction): void;Defined in: Retree.ts:914
Run a synchronous transaction that will not cause Retree.on listeners for changed nodes to emit multiple times.
Parameters#
| Parameter | Type | Description |
|---|---|---|
transaction | () => void | transaction function to run |
Returns#
void
Remarks#
If multiple nodes changed during the transaction, Retree.on events will be emitted for each node that changed.
If using React, this should still be flattened to a single render, but it is not guaranteed.
It may be reasonable to combine this with React.startTransition if this is a concern.
Example#
const counter = Retree.root({ count: 0 });
Retree.on(counter, "nodeChanged", () => console.log(counter.count));
// Will only emit "nodeChanged" once
Retree.runTransaction(() => {
counter.count = counter.count + 1;
counter.count = counter.count * 2;
});select()#
Call Signature#
static select<TNode, TSelected>(
node,
selector,
callback,
options?): () => void;Defined in: Retree.ts:528
Subscribe to a derived value from any Retree-managed node.
Type Parameters
| Type Parameter |
|---|
TNode extends object |
TSelected |
Parameters
| Parameter | Type | Description |
|---|---|---|
node | TNode | Retree-managed node to observe. |
selector | RetreeSelectSelector<TNode, TSelected> | Function that reads a selected value or dependency list from the latest reproxy. |
callback | (next, previous) => void | Called only when the selected value or dependency list changes. |
options? | RetreeSelectOptions<TSelected> | Optional listener type and equality comparison for the whole selected value or tuple. |
Returns
Unsubscribe function.
() => void
Remarks
select recomputes the selected value when the observed node or selected
reactive dependencies emit, then calls callback only when the selection
changes. Selectors may return one value or an ordered dependency list.
Reactive entries in a dependency list are subscribed to; primitive and
plain entries are compared by identity.
Dependency-list subscriptions are observational: if a selected dependency
emits, select calls your callback when the selection changes, but it
does not force the node passed to select to receive a fresh reproxy.
Use @select when a ReactiveNode owner should emit nodeChanged.
This is a subscription primitive, not a memo cache: use memo /
fnMemo to cache computation, and select to narrow notifications.
By default select listens to nodeChanged, which is correct when the
selector reads fields directly owned by node. Pass
listenerType: "treeChanged" when the selector intentionally reads
descendants that are not included as reactive entries in a dependency
list.
You can also call Retree.select(() => value, callback) without a node.
That form traps reads automatically. Whole Retree-managed values are
subscribed to broadly. Property reads subscribe to the owner node but
compare the specific property value, so task.done can react to task
replacement or done changes without reacting to unrelated task fields.
Primitive reads are kept as comparison values, so the callback only runs
when the trapped reads make the selected value or dependency set change.
Examples
const project = Retree.root({
tasks: [{ done: false }, { done: true }],
});
const unsubscribe = Retree.select(
project.tasks,
(tasks) => tasks.filter((task) => task.done).length,
(next, previous) => console.log({ next, previous }),
{ listenerType: "treeChanged" }
);
project.tasks[0].done = true; // ✅ callback: 1 -> 2
project.tasks[0].done = true; // ❌ selected value did not change
unsubscribe();Retree.select(
row,
(self) => [self.attributes, self.attributeId, self.attribute],
([, , attribute]) => console.log(attribute)
);Retree.select(
() => project.tasks.filter((task) => task.done).length,
(doneCount) => console.log(doneCount)
);Call Signature#
static select<TSelector>(
selector,
callback,
options?): () => void;Defined in: Retree.ts:534
Subscribe to a derived value from any Retree-managed node.
Type Parameters
| Type Parameter |
|---|
TSelector extends RetreeTrackedSelectSelector<unknown> |
Parameters
| Parameter | Type | Description |
|---|---|---|
selector | TSelector | Function that reads a selected value or dependency list from the latest reproxy. |
callback | (next, previous) => void | Called only when the selected value or dependency list changes. |
options? | RetreeSelectOptions<ReturnType<TSelector>> | Optional listener type and equality comparison for the whole selected value or tuple. |
Returns
Unsubscribe function.
() => void
Remarks
select recomputes the selected value when the observed node or selected
reactive dependencies emit, then calls callback only when the selection
changes. Selectors may return one value or an ordered dependency list.
Reactive entries in a dependency list are subscribed to; primitive and
plain entries are compared by identity.
Dependency-list subscriptions are observational: if a selected dependency
emits, select calls your callback when the selection changes, but it
does not force the node passed to select to receive a fresh reproxy.
Use @select when a ReactiveNode owner should emit nodeChanged.
This is a subscription primitive, not a memo cache: use memo /
fnMemo to cache computation, and select to narrow notifications.
By default select listens to nodeChanged, which is correct when the
selector reads fields directly owned by node. Pass
listenerType: "treeChanged" when the selector intentionally reads
descendants that are not included as reactive entries in a dependency
list.
You can also call Retree.select(() => value, callback) without a node.
That form traps reads automatically. Whole Retree-managed values are
subscribed to broadly. Property reads subscribe to the owner node but
compare the specific property value, so task.done can react to task
replacement or done changes without reacting to unrelated task fields.
Primitive reads are kept as comparison values, so the callback only runs
when the trapped reads make the selected value or dependency set change.
Examples
const project = Retree.root({
tasks: [{ done: false }, { done: true }],
});
const unsubscribe = Retree.select(
project.tasks,
(tasks) => tasks.filter((task) => task.done).length,
(next, previous) => console.log({ next, previous }),
{ listenerType: "treeChanged" }
);
project.tasks[0].done = true; // ✅ callback: 1 -> 2
project.tasks[0].done = true; // ❌ selected value did not change
unsubscribe();Retree.select(
row,
(self) => [self.attributes, self.attributeId, self.attribute],
([, , attribute]) => console.log(attribute)
);Retree.select(
() => project.tasks.filter((task) => task.done).length,
(doneCount) => console.log(doneCount)
);untracked()#
static untracked<T>(fn): T;Defined in: Retree.ts:780
Run a synchronous function with Retree dependency tracking paused.
Type Parameters#
| Type Parameter |
|---|
T |
Parameters#
| Parameter | Type | Description |
|---|---|---|
fn | () => T | Function to run without dependency tracking. |
Returns#
T
The function's return value.
Remarks#
Inside tracked contexts — Retree.select(() => ...), useSelect
selectors, and auto-trapped @memo / @fnMemo / @select bodies —
every Retree read is recorded as a dependency. Wrap bulk reads in
untracked when they should not subscribe, such as a wide scan whose
result is already covered by a narrower dependency.
Reads inside untracked still go through Retree proxies (combine with
Retree.raw for native-speed scans). Writes inside untracked
still emit normally; this pauses dependency collection, not change
emission.
Example#
const doneCount = Retree.select(
() => {
const tasks = project.tasks; // ✅ tracked: subscribes to tasks
return Retree.untracked(
() => tasks.filter((task) => task.done).length
);
},
(count) => console.log(count)
);use()#
static use<T>(object): T;Defined in: Retree.ts:187
Type Parameters#
| Type Parameter | Default type |
|---|---|
T extends object | object |
Parameters#
| Parameter | Type |
|---|---|
object | T |
Returns#
T
Deprecated#
Use root instead.
Example#
const state = Retree.use({ count: 0 }); // deprecated
const state = Retree.root({ count: 0 }); // preferred