Skip to main content

Comparison with mobx-state-tree and mobx-keystone

This library is very much like mobx-state-tree/mobx-keystone and takes lots of ideas from them, so the transition should be fairly simple. There are some trade-offs though, as shown in the following chart:

Feature Comparison

Featuremobx-bonsaimobx-state-treemobx-keystone
Fast and low memory usage
Tree-like structure
Immutable snapshot generation
TypeScript support (1)
Simplified instance / snapshot type usage (2)
Model life-cycle support (3)
Runtime type validation (4)
No metadata inside snapshots (5)
Map/Set support (6)
Patch generation
Action serialization / replaying
Action middleware support
- Atomic/Transaction middleware
- Undo manager middleware (7)
Flow action support
References
Frozen data
Redux compatibility layer🟠 Not yet
Y.js binding

Feature Notes

  1. Support for self references / cross references / no need for late types, no need for casting, etc. It just uses Typescript to define your models.
  2. Simplified is an understatement, because these types of cast just don't even exist in mobx-bonsai.
  3. It is possible to attach an init event using onNodeInit (as long as nodes include a $$type property) and it is possible to detect when they attach/detach from a tree or a parent using onChildAttachedTo.
  4. A library like zod might be used to do the type validation and infer the Typescript type.
  5. A $$type property may be included if onInit support is needed, but it is optional.
  6. asMap and asSet wrappers are offered to be able to manipulate objects as if they were maps and arrays as if they were sets.
  7. The Y.js binding has an undo manager though, so if you use that binding that should be covered.

TypeScript Improvements

mobx-state-tree has some limitations when it comes to TypeScript typings, which are not problems at all with mobx-bonsai since it just uses plain Typescript types.

Also, self-recursive or cross-referenced models are impossible (or at least very hard) to properly type in mobx-state-tree, but, again, they are no problem with mobx-bonsai.

Simpler Instance / Snapshot Type Usage

Another area of improvement is the simplification of the usage of snapshot vs. instance types. In mobx-state-tree it is possible to assign snapshots to properties, as well as actual instances, but the actual type of properties are instances, which leads to confusing casts and constructs such as:

// mobx-state-tree code

const Todo = types
.model({
done: false,
text: types.string,
})
.actions((self) => ({
setText(text: string) {
self.text = text
},
setDone(done: boolean) {
self.done = done
},
}))

const RootStore = types
.model({
selected: types.maybe(Todo),
})
.actions((self) => ({
// note the usage of a union of the snapshot type and the instance type
setSelected(todo: SnapshotIn<typeof Todo> | Instance<typeof Todo>) {
// note the usage of cast to indicate that it is ok to use a snapshot when
// the property actually expects an instance
self.selected = cast(todo)
},
}))

In mobx-bonsai, since "models" are always just data structures, you just don't have that problem - there's always only one type for all operations, and snapshots are exactly the same type as the node.

Less Confusion Between this/self Usages

Usually in mobx-state-tree code from a previous "chunk" (actions, views) has to be accessed using self, while code in the same "chunk" has to be accessed using this to get proper typings:

// mobx-state-tree code

const Todo = types
.model({
done: false,
text: types.string,
title: types.string,
})
.views((self) => ({
get asStr() {
// here we use `self` since the properties come from a previous chunk
return `${self.text} is done? ${self.done}`
},
get asStrWithTitle() {
// here we use `this` for `asStr` since it comes from the current chunk
return `${self.title} - ${this.asStr}`
},
}))

In mobx-bonsai, since it uses functions for actions/getters/computed/volatiles etc the problem does not apply (no this/self anywhere).

Simplified Model Life-cycle

mobx-state-tree has a couple of life-cycle hooks (afterCreate, afterAttach, beforeDetach, beforeCreate) that might or might not trigger when you think they should due to the lazy initialization of nodes.

For example, you might create a submodel with an afterCreate hook, but it might never be actually executed unless the node contents are accessed (due to lazy initialization). Maybe you might want to set up an effect (reaction or the like), but you only want that effect to work after it actually becomes part of your application state. Likewise, you might want to call getRoot to access the root model, but it might actually not give the value you expect until the model is attached to a parent which is eventually (or not) attached to the proper root.

mobx-bonsai solves this by offering a onChildAttachedTo method that you can call for example in your root node to know when a node comes/exits the root store thus ensuring that at that point getRoot will return the expected value and makes it a perfect place to set up effects.

For initialization you may either use onNodeInit to register what should happen when a node is manually or auto-created (for example useful for migrations) or if you need a initialization/constructor function you may do it in a functional way:

const createTodo = (todo: Partial<Todo>): Todo {
return node({ done: false, text: "", ...todo });
}

Speed Comparison

Here's a benchmark between mobx-bonsai and mobx-state-tree with type checking disabled.

empty creation (non type checked props)
mobx-bonsai x 34,376 ops/sec ±0.68% (93 runs sampled)
mobx-state-tree x 10,499 ops/sec ±0.37% (98 runs sampled)
Fastest between mobx-bonsai and mobx-state-tree is mobx-bonsai by 3.27x

empty creation + access all simple props (non type checked props)
mobx-bonsai x 32,647 ops/sec ±0.69% (94 runs sampled)
mobx-state-tree x 5,497 ops/sec ±0.40% (97 runs sampled)
Fastest between mobx-bonsai and mobx-state-tree is mobx-bonsai by 5.94x

already created, access all simple props (non type checked props)
mobx-bonsai x 729,410 ops/sec ±0.18% (98 runs sampled)
mobx-state-tree x 588,746 ops/sec ±0.19% (97 runs sampled)
Fastest between mobx-bonsai and mobx-state-tree is mobx-bonsai by 1.24x

snapshot creation (non type checked props)
mobx-bonsai x 33,896 ops/sec ±0.66% (96 runs sampled)
mobx-state-tree x 10,919 ops/sec ±0.36% (94 runs sampled)
Fastest between mobx-bonsai and mobx-state-tree is mobx-bonsai by 3.10x

already created, change all simple props (non type checked props)
mobx-bonsai x 90,406 ops/sec ±7.39% (94 runs sampled)
mobx-state-tree x 9,962 ops/sec ±0.21% (97 runs sampled)
Fastest between mobx-bonsai and mobx-state-tree is mobx-bonsai by 9.08x

already created, change one simple props + getSnapshot (non type checked props)
mobx-bonsai x 257,759 ops/sec ±0.27% (97 runs sampled)
mobx-state-tree x 94,567 ops/sec ±0.37% (94 runs sampled)
Fastest between mobx-bonsai and mobx-state-tree is mobx-bonsai by 2.73x