The gist of MobX
Concepts
MobX distinguishes between the following three concepts in your application:
- State
- Actions
- Derivations
Let's take a closer look at these concepts below, or alternatively, in the 10 minute introduction to MobX and React, where you can interactively dive deeper into these concepts step by step and build a simple Todo list app.
Some might recognise the concept of "Signals" in the concepts described below. This is correct, MobX is a signal based state management library avant la lettre.
1. Define state and make it observable
State is the data that drives your application. Usually, there is domain specific state like a list of todo items, and there is view state, such as the currently selected element. State is like spreadsheet cells that hold a value.
Store state in any data structure you like: plain objects, arrays, classes, cyclic data structures or references. It doesn't matter for the workings of MobX.
Just make sure that all properties you want to change over time are marked as observable
so MobX can track them.
Here is a simple example:
import { makeObservable, observable, action } from "mobx"
class Todo {
id = Math.random()
title = ""
finished = false
constructor(title) {
makeObservable(this, {
title: observable,
finished: observable,
toggle: action
})
this.title = title
}
toggle() {
this.finished = !this.finished
}
}
Using observable
is like turning a property of an object into a spreadsheet cell.
But unlike spreadsheets, these values can not only be primitive values, but also references, objects and arrays.
Tip: Prefer classes, plain objects or decorators? MobX supports many styles.
This example can be shortened using makeAutoObservable
, but by being explicit we can showcase the different concepts in greater detail.
Note that MobX doesn't dictate an object style, plain objects instead can be used as well, as can decorators for even more concise classes. See the page for more details.
But what about toggle
, which we marked as action
?
2. Update state using actions
An action is any piece of code that changes the state. User events, backend data pushes, scheduled events, etc. An action is like a user that enters a new value into a spreadsheet cell.
In the Todo
model above you can see that we have a toggle
method that changes the value of finished
. finished
is marked as observable
. It is recommended that you mark any piece of code that changes observable
's as an action
. That way MobX can automatically apply transactions for effortless optimal performance.
Using actions helps you structure your code and prevents you from inadvertently changing state when you don't intend to. Methods that modify state are called actions in MobX terminology. In contrast to views, which compute new information based on the current state. Every method should serve at most one of those two goals.
3. Create derivations that automatically respond to state changes
Anything that can be derived from the state without any further interaction is a derivation. Derivations exist in many forms:
- The user interface
- Derived data, such as the number of remaining
todos
- Backend integrations, e.g. sending changes to the server
MobX distinguishes between two kinds of derivations:
- Computed values, which can always be derived from the current observable state using a pure function
- Reactions, side effects that need to happen automatically when the state changes (bridge between imperative and reactive programming)
When starting with MobX, people tend to overuse reactions.
The golden rule is, always use computed
if you want to create a value based on the current state.
3.1. Model derived values using computed
To create a computed value, define a property using a JS getter function get
and mark it as computed
with makeObservable
.
import { makeObservable, observable, computed } from "mobx"
class TodoList {
todos = []
get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length
}
constructor(todos) {
makeObservable(this, {
todos: observable,
unfinishedTodoCount: computed
})
this.todos = todos
}
}
MobX will ensure that unfinishedTodoCount
is updated automatically when a todo is added or when one of the finished
properties is modified.
These computations resemble formulas in spreadsheet programs like MS Excel. They update automatically, but only when required. That is, if something is interested in their outcome.
3.2. Model side effects using reactions
For you as a user to be able to see a change in state or computed values on the screen, a reaction that repaints a part of the GUI is needed.
Reactions are similar to computed values, but instead of producing information, they produce side effects like printing to the console, making network requests, incrementally updating React component tree to patch the DOM, etc.
In short, reactions bridge the worlds of reactive and imperative programming.
By far the most used form of reactions are UI components. Note that it is possible to trigger side effects from both actions and reactions. Side effects that have a clear, explicit origin from which they can be triggered, such as making a network request when submitting a form, should be triggered explicitly from the relevant event handler.
3.3. Reactive React components
If you are using React, you can make your components reactive by wrapping them with the observer
function from the bindings package you've chosen during installation. In this example, we're going to use the more lightweight mobx-react-lite
package.
import * as React from "react"
import { render } from "react-dom"
import { observer } from "mobx-react-lite"
const TodoListView = observer(({ todoList }) => (
<div>
<ul>
{todoList.todos.map(todo => (
<TodoView todo={todo} key={todo.id} />
))}
</ul>
Tasks left: {todoList.unfinishedTodoCount}
</div>
))
const TodoView = observer(({ todo }) => (
<li>
<input type="checkbox" checked={todo.finished} onClick={() => todo.toggle()} />
{todo.title}
</li>
))
const store = new TodoList([new Todo("Get Coffee"), new Todo("Write simpler code")])
render(<TodoListView todoList={store} />, document.getElementById("root"))
observer
converts React components into derivations of the data they render.
When using MobX there are no smart or dumb components.
All components render smartly, but are defined in a dumb manner. MobX will simply make sure the components are always re-rendered whenever needed, and never more than that.
So the onClick
handler in the above example will force the proper TodoView
component to re-render as it uses the toggle
action, but will only cause the TodoListView
component to re-render if the number of unfinished tasks has changed.
And if you would remove the Tasks left
line (or put it into a separate component), the TodoListView
component would no longer re-render when ticking a task.
To learn more about how React works with MobX, check out the React integration section.
3.4. Custom reactions
You will need them rarely, but they can be created using the autorun
,
reaction
or when
functions to fit your specific situations.
For example, the following autorun
prints a log message every time the amount of unfinishedTodoCount
changes:
// A function that automatically observes the state.
autorun(() => {
console.log("Tasks left: " + todos.unfinishedTodoCount)
})
Why does a new message get printed every time the unfinishedTodoCount
is changed? The answer is this rule of thumb:
MobX reacts to any existing observable property that is read during the execution of a tracked function.
To learn more about how MobX determines which observables need to be reacted to, check out the Understanding reactivity section.
Principles
MobX uses a uni-directional data flow where actions change the state, which in turn updates all affected views.
All derivations are updated automatically and atomically when the state changes. As a result, it is never possible to observe intermediate values.
All derivations are updated synchronously by default. This means that, for example, actions can safely inspect a computed value directly after altering the state.
Computed values are updated lazily. Any computed value that is not actively in use will not be updated until it is needed for a side effect (I/O). If a view is no longer in use it will be garbage collected automatically.
All computed values should be pure. They are not supposed to change state.
To learn more about the background context, check out the fundamental principles behind MobX.
Try it out!
You can play with the above examples yourself on CodeSandbox.
Linting
If you find it hard to adopt the mental model of MobX, configure it to be very strict and warn you at runtime whenever you deviate from these patterns. Check out the linting MobX section.