Overview & Philosophy
mobx-state-tree
(also known as "MST") is a state container that combines the simplicity and ease of mutable data with the traceability of immutable data and the reactiveness and performance of observable data.
Simply put, MST tries to combine the best features of both immutability (transactionality, traceability and composition) and mutability (discoverability, co-location and encapsulation) based approaches to state management; everything to provide the best developer experience possible. Unlike MobX itself, MST is very opinionated about how data should be structured and updated. This makes it possible to solve many common problems out of the box.
Central in MST is the concept of a living tree. The tree consists of mutable, but strictly protected objects enriched with runtime type information. In other words, each tree has a shape (type information) and state (data). From this living tree, immutable, structurally shared, snapshots are automatically generated.
import { types, onSnapshot } from "mobx-state-tree"
const Todo = types
.model("Todo", {
title: types.string,
done: false
})
.actions((self) => ({
toggle() {
self.done = !self.done
}
}))
const Store = types.model("Store", {
todos: types.array(Todo)
})
// create an instance from a snapshot
const store = Store.create({
todos: [
{
title: "Get coffee"
}
]
})
// listen to new snapshots
onSnapshot(store, (snapshot) => {
console.dir(snapshot)
})
// invoke action that modifies the tree
store.todos[0].toggle()
// prints: `{ todos: [{ title: "Get coffee", done: true }]}`
By using the type information available, snapshots can be converted to living trees, and vice versa, with zero effort. Because of this, time travelling is supported out of the box, and tools like HMR are trivial to support, see this HMR example.
The type information is designed in such a way that it is used both at design- and run-time to verify type correctness (Design time type checking works in TypeScript only at the moment; Flow PR's are welcome!)
[mobx-state-tree] Value '{\"todos\":[{\"turtle\":\"Get tea\"}]}' is not assignable to type: Store, expected an instance of Store or a snapshot like '{ todos: { title: string; done: boolean }[] }' instead.
Runtime type error
Designtime type error
Because state trees are living, mutable models, actions are straight-forward to write; just modify local instance properties where appropriate. See the toggle()
-action in the Todo-store above or the examples below. It is not necessary to produce a new state tree yourself, MST's snapshot functionality will derive one for you automatically.
Although mutable sounds scary to some, fear not, actions have many interesting properties. By default trees can only be modified by using an action that belongs to the same subtree. Furthermore, actions are replayable and can be used to distribute changes (example).
Moreover, because changes can be detected on a fine grained level, JSON patches are supported out of the box. Simply subscribing to the patch stream of a tree is another way to sync diffs with, for example, back-end servers or other clients (example).
Since MST uses MobX behind the scenes, it integrates seamlessly with mobx and mobx-react-lite (or mobx-react). See also this egghead.io lesson: Render mobx-state-tree Models in React. Even cooler, because it supports snapshots, middleware and replayable actions out of the box, it is possible to replace a Redux store and reducer with a MobX state tree. This makes it possible to connect the Redux devtools to MST. See the Redux / MST TodoMVC example.
For futher reading: the conceptual difference between snapshots, patches and actions in relation to distributing state changes is extensively discussed in this blog post
Finally, MST has built-in support for references, identifiers, dependency injection, change recording and circular type definitions (even across files). Even fancier, it analyses liveliness of objects, failing early when you try to access accidentally cached information! (More on that later)
A unique feature of MST is that it offers liveliness guarantees. MST will throw an exception when reading or writing from objects that are no longer part of a state tree. This protects you against accidental stale reads of objects still referred by, for example, a closure.
const oldTodo = store.todos[0]
store.removeTodo(0)
function logTodo(todo) {
setTimeout(() => console.log(todo.title), 1000)
}
logTodo(store.todos[0])
store.removeTodo(0)
// throws exception in one second for using an stale object!
Despite all that, you will see that in practice the API is quite straightforward!
Another way to look at mobx-state-tree is to consider it, as argued by Daniel Earwicker, to be "React, but for data". Like React, MST consists of composable components, called models, which captures a small piece of state. They are instantiated from props (snapshots) and after that manage and protect their own internal state (using actions). Moreover, when applying snapshots, tree nodes are reconciled as much as possible. There is even a context-like mechanism, called environments, to pass information to deep descendants.
An introduction to the philosophy can be watched here. Slides. Or, as markdown to read it quickly.
mobx-state-tree "immutable trees" and "graph model" features talk, "Next Generation State Management" at React Europe 2017. Slides.