Tree-Like Structure
Overviewβ
State as a Tree Structureβ
mobx-bonsai organizes your application state as an intuitive tree of observable nodes, providing a natural mental model that matches how most UI hierarchies work.
Each node in your state tree can be one of:
- π¦ An observable plain object (for structured data)
- π An observable array (for collections)
- π§ A primitive value (
string,boolean,number,null,undefined)
Key Tree Rulesβ
This tree-based architecture follows these simple rules:
- π A non-primitive (object) node can have zero or one parent.
- πΏ A non-primitive (object) node can have zero to infinite children.
- π From rules 1 and 2, a non-primitive node can exist in only one place in the tree.
- π Primitive nodes are always copied by value, so they aren't subject to the rules above.
Node Transformationβ
Objects and arrays are automatically transformed into tree nodes when:
- You explicitly call
node()on them - They're added as children to an existing tree node
π‘ Important Detail: When a plain object is added to a tree, its reference changes as it becomes a node. This is standard behavior in MobX deep observables:
const todo = { text: "Buy milk", done: false };
todoAppState.todoList.push(todo);
// todoAppState.todoList[0] !== todo
Creating Nodesβ
import { node } from 'mobx-bonsai';
// Create a new node
const todoNode = node({
text: 'Buy milk',
done: false
});
Checking if something is a nodeβ
import { isNode } from 'mobx-bonsai';
// Returns true if value is a tree node
const isTreeNode = isNode(someObject);
Traversal Methodsβ
Parent-Child Navigationβ
Nodes provide powerful methods to navigate the tree structure:
getParentPathβ
getParentPath<T extends object>(value: object): ParentPath<T> | undefined
Returns the parent of the target plus the path from the parent to the target, or undefined if it has no parent.
getParentβ
getParent<T extends object>(value: object): T | undefined
Returns the parent object of the target object, or undefined if there's no parent.
getParentToChildPathβ
getParentToChildPath(fromParent: object, toChild: object): Path | undefined
Gets the path to get from a parent to a given child.
Returns an empty array if the child is actually the given parent or undefined if the child is not a child of the parent.
getRootPathβ
getRootPath<T extends object>(value: object): RootPath<T>
Returns the root of the target, the path from the root to get to the target and the list of objects from root (included) until target (included).
getRootβ
getRoot<T extends object>(value: object): T
Returns the root of the target object, or itself if the target is a root.
isRootβ
isRoot(value: object): boolean
Returns true if a given object is a root object.
isChildOfParentβ
isChildOfParent(child: object, parent: object): boolean
Returns true if the target is a "child" of the tree of the given "parent" object.
isParentOfChildβ
isParentOfChild(parent: object, child: object): boolean
Returns true if the target is a "parent" that has in its tree the given "child" object.
Path Resolution and Findingβ
resolvePathβ
resolvePath<T extends object>(pathRootObject: object, path: Path): { resolved: true; value: T } | { resolved: false }
Resolves a path from an object, returning an object with { resolved: true, value: T } or { resolved: false }.
findParentβ
findParent<T extends object>(child: object, predicate: (parent: object) => boolean, maxDepth = 0): T | undefined
Iterates through all the parents (from the nearest until the root) until one of them matches the given predicate.
If the predicate is matched it will return the found node.
If none is found it will return undefined.
A max depth of 0 is infinite, but another one can be given.
findParentPathβ
findParentPath<T extends object>(child: object, predicate: (parent: object) => boolean, maxDepth = 0): FoundParentPath<T> | undefined
Iterates through all the parents (from the nearest until the root) until one of them matches the given predicate.
If the predicate is matched it will return the found node and the path from the parent to the child.
If none is found it will return undefined.
A max depth of 0 is infinite, but another one can be given.
findChildrenβ
findChildren<T extends object>(root: object, predicate: (node: object) => boolean, options?: { deep?: boolean }): ReadonlySet<T>
Iterates through all children and collects them in a set if the given predicate matches.
Pass the options object with the deep option (defaults to false) set to true to get the children deeply or false to get them shallowly.
getChildrenObjectsβ
getChildrenObjects(node: object, options?: { deep?: boolean }): ReadonlySet<object>
Returns a set with all the children objects (this is, excluding primitives) of an object.
Pass the options object with the deep option (defaults to false) set to true to get the children deeply or false to get them shallowly.
Tree Traversalβ
walkTreeβ
walkTree<T = void>(target: object, predicate: (node: object) => T | undefined, mode: WalkTreeMode): T | undefined
Walks a tree, running the predicate function for each node.
If the predicate function returns something other than undefined then the walk will be stopped and the function will return the returned value.
The mode can be one of:
WalkTreeMode.ParentFirst- The walk will be done parent (roots) first, then children.WalkTreeMode.ChildrenFirst- The walk will be done children (leaves) first, then parents.
Utility Methodsβ
Child Attachment Trackingβ
onChildAttachedToβ
Monitor when nodes are attached or detached from the tree:
export function onChildAttachedTo<T extends object = object>({
target,
childNodeType,
onChildAttached,
deep,
fireForCurrentChildren,
}: {
target: () => object
childNodeType: AnyTypedNodeType | readonly AnyTypedNodeType[] | undefined
onChildAttached: (child: T) => (() => void) | void
deep?: boolean
fireForCurrentChildren?: boolean
}): (runDetachDisposers: boolean) => void
Runs a callback every time a new object is attached to a given node. The callback can optionally return a disposer function which will be run when the child is detached.
Parameters:
target: Function that returns the node whose children should be tracked.childNodeType: The node type (or array of types) for which the callback should be invoked, orundefinedif it should be invoked for all node types.onChildAttached: Callback called when a child is attached to the target node. Can return a cleanup function to run when the child is detached.deep: (default:false) Whentrue, watches for children attached at any level of the tree. Whenfalse, only watches for direct children.fireForCurrentChildren: (default:true) Whentrue, the callback will be immediately executed for all matching children that are already attached.
Returns a disposer function that accepts a boolean parameter:
- When called with
true, all pending detach disposers for children that had the attach event fired will be executed. - When called with
false, the tracking stops but doesn't run detach disposers.
Example:
const disposer = onChildAttachedTo({
target: () => todoListNode,
childNodeType: TTodo, // Only run for todo nodes
onChildAttached: (todoNode) => {
console.log(`Todo "${todoNode.text}" was added!`);
// Optional: Return a cleanup function
return () => {
console.log(`Todo "${todoNode.text}" was removed!`);
};
},
deep: false, // Only watch direct children
fireForCurrentChildren: true // Run for existing todos
});
// Later, to clean up and run all detach disposers:
disposer(true);
Deep Comparisonβ
deepEqualsβ
deepEquals(a: any, b: any): boolean
Deeply compares two values with optimized handling for mobx-bonsai nodes.
Supported value types:
- Primitives
- Boxed observables
- Objects, observable objects
- Arrays, observable arrays
- Typed arrays
- Maps, observable maps
- Sets, observable sets
- Tree nodes (optimized by using snapshot comparison internally)
Note that in the case of models the result will be false if their model IDs are different.
import { deepEquals } from 'mobx-bonsai';
// Compare two snapshots or nodes
if (deepEquals(snapshot1, snapshot2)) {
console.log('The states are equivalent');
}