Skip to main content

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:

  1. πŸ”— A non-primitive (object) node can have zero or one parent.
  2. 🌿 A non-primitive (object) node can have zero to infinite children.
  3. πŸ”„ From rules 1 and 2, a non-primitive node can exist in only one place in the tree.
  4. πŸ“ 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, or undefined 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) When true, watches for children attached at any level of the tree. When false, only watches for direct children.
  • fireForCurrentChildren: (default: true) When true, 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');
}