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, orundefined
if 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');
}