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
Feature | mobx-bonsai | mobx-state-tree | mobx-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
- Support for self references / cross references / no need for late types, no need for casting, etc. It just uses Typescript to define your models.
- Simplified is an understatement, because these types of cast just don't even exist in
mobx-bonsai
. - 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 usingonChildAttachedTo
. - A library like
zod
might be used to do the type validation and infer the Typescript type. - A
$$type
property may be included if onInit support is needed, but it is optional. asMap
andasSet
wrappers are offered to be able to manipulate objects as if they were maps and arrays as if they were sets.- 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