Tree-Like Structure
Overviewβ
mobx-keystone's structure is based on a tree-like structure, where each node can be one of:
- A model instance.
- A plain object.
- An array.
- A primitive value (
string,boolean,number,null,undefined).
About arrays, it is interesting to note that by default they cannot hold undefined values, but they can hold null values. This rule is enforced to ensure compatibility with JSON. If you really need arrays with undefined values, it can be enabled in the global configuration:
setGlobalConfig({
allowUndefinedArrayElements: true,
})
Since the structure is a tree, this means these tree rules apply:
- A non-primitive (object) node can have zero or one parent.
- A non-primitive (object) node can have zero to infinite children.
- From rule 1 and 2 we can extract that a same non-primitve node can only be in a single tree and only once.
- Primitive nodes are always copied by value (as usual in JavaScript), so none of the rules above apply.
- Note that class models with the
valueType: trueoption will get cloned automatically before getting inserted as a child of another node so, for all practical purposes, rule 3 does not apply and acts more akin to a primitive.
As an example of rule 1, this would not be allowed:
// given `someModel`, `someOtherModel`, `someArray`
// ok, `someArray` has now one parent and becomes a tree node object
someModel.setArray(someArray)
// but this would throw since `someArray` is already a tree node object which already has one parent
someOtherModel.setArray(someArray)
But as rule 4 states, this would be ok:
// given `someModel`, `someOtherModel`
const somePrimitive = "hi!"
// ok, the primitive is copied, and has now one parent
someModel.setPrimitive(somePrimitive)
// ok too, since the primitive is copied again, and has one parent
someOtherModel.setPrimitive(somePrimitive)
A way to work around rule 1 is possible thanks to the use of references as shown in the references section.
How objects are transformed into nodesβ
A model/object/array is turned into a tree node under the following circumstances:
- Model instances are always tree nodes.
- Plain objects / arrays are turned into tree nodes as soon as they become children of another tree node.
To check if a non-primitive has been turned into a tree node you can use isTreeNode(value: object): boolean, or
assertIsTreeNode(value: object, argName: string = "argument"): asserts value is object to assert it.
To turn a non-primitive into a tree node you can use toTreeNode<T>(value: T): T. If the object is already a tree node then the same object will be returned.
Additionally, toTreeNode<TType, V>(type: TType, value: V): V can be used with a type checker which will be invoked to check the data (when auto model type checking is enabled) if desired.
Traversal methodsβ
When a non-primitive value is turned into a tree node it gains access to certain methods that allow traversing the data tree:
getParentPathβ
getParentPath<T extends object = any>(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 = any>(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.
isModelDataObjectβ
isModelDataObject(value: object): boolean
Returns true if a given object is a model interim data object ($).
getRootPathβ
getRootPath<T extends object = any>(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 = any>(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.
resolvePathβ
resolvePath<T = any>(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 = any>(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 = any>(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 = any>(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 an observable 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.
walkTreeβ
walkTree<T = void>(target: object, predicate: (node: any) => 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β
detachβ
detach(value: object): void
Besides the aforementioned isTreeNode, assertIsTreeNode and toTreeNode functions, there's also the detach(value: object) function, which allows a node to get detached from its parent following this logic:
- If the parent is an object / model, detaching will delete the property.
- If the parent is an array detaching will remove the node by splicing it.
- If there's no parent it will throw.
onChildAttachedToβ
onChildAttachedTo(target: () => object, fn: (child: object) => (() => void) | void, options?: { 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 which will be run when the child is detached.
The optional options parameter accepts an object with the following options:
deep: boolean(default:false) -trueif the callback should be run for all children deeply orfalseif it it should only run for shallow children.fireForCurrentChildren: boolean(default:true) -trueif the callback should be immediately called for currently attached children,falseif only for future attachments.
Returns a disposer, which has a boolean parameter which should be true if pending detachment callbacks should be run, or false otherwise.
applySetβ
applySet<O extends object, K extends keyof O, V extends O[K]>(node: O, fieldName: K, value: V): void
Allows setting an object/model field / array index to a given value without the need to wrap it in modelAction. Unlike runUnprotected, this is actually an action that can be captured and replicated.
applySet(someModel, "prop", "value")
applyDeleteβ
applyDelete<O extends object, K extends keyof O>(node: O, fieldName: K): void
Allows deleting an object field / array index without the need to wrap it in modelAction. Unlike runUnprotected, this is actually an action that can be captured and replicated.
applyDelete(someObject, "field")
applyMethodCallβ
applyMethodCall<O extends object, K extends keyof O, FN extends O[K]>(node: O, methodName: K, ...args: Parameters<FN> : ReturnType<FN>
Allows calling an model/object/array method without the need to wrap it in modelAction. Unlike runUnprotected, this is actually an action that can be captured and replicated.
const newArrayLength = applyMethodCall(someArray, "push", 1, 2, 3)
deepEqualsβ
deepEquals(a: any, b: any): boolean
Deeply compares two values.
Supported values are:
- 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.