A single domain store should be responsible for a single concept in your application. However a single concept might take the form of multiple subtypes and it is often a (cyclic) tree structure. For example: one domain store for your products, and one for our orders and orderlines. As a rule of thumb: if the nature of the relationship between two items is containment, they should typically be in the same store. So a store just manages domain objects.
These are the responsibility of a store:
- Instantiate domain objects. Make sure domain objects know the store they belong to.
- Make sure there is only one instance of each of your domain objects. The same user, order or todo should not be twice in your memory. This way you can safely use references and also be sure you are looking at the latest instance, without ever having to resolve a reference. This is fast, straightforward and convenient when debugging.
- Provide backend integration. Store data when needed.
- Update existing instances if updates are received from the backend.
- Provide a stand-alone, universal, testable component of your application.
- To make sure your store is testable and can be run server-side, you probably will move doing actual websocket / http requests to a separate object so that you can abstract over your communication layer.
- There should be only one instance of a store.
Domain objects
Each domain object should be expressed using its own class (or constructor function). It is recommended to store your data in denormalized form. There is no need to treat your client-side application state as some kind of database. Real references, cyclic data structures and instance methods are powerful concepts in JavaScript. Domain objects are allowed to refer directly to domain objects from other stores. Remember: we want to keep our actions and views as simple as possible and needing to manage references and doing garbage collection yourself might be a step backward. Unlike many Flux architectures, with MobX there is no need to normalize your data, and this makes it a lot simpler to build the essentially complex parts of your application: your business rules, actions and user interface.
Domain objects can delegate all their logic to the store they belong to if that suits your application well. It is possible to express your domain objects as plain objects, but classes have some important advantages over plain objects:
- They can have methods. This makes your domain concepts easier to use stand-alone and reduces the amount of contextual awareness that is needed in your application. Just pass objects around. You don't have to pass stores around, or have to figure out which actions can be applied to an object if they are just available as instance methods. Especially in large applications this is important.
- They offer fine grained control over the visibility of attributes and methods.
- Objects created using a constructor function can freely mix observable properties and functions, and non-observable properties and methods.
- They are easily recognizable and can strictly be type-checked.
Example domain store
import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';
export class TodoStore {
authorStore;
transportLayer;
@observable todos = [];
@observable isLoading = true;
constructor(transportLayer, authorStore) {
this.authorStore = authorStore;
this.transportLayer = transportLayer;
this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
this.loadTodos();
}
loadTodos() {
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
}
updateTodoFromServer(json) {
var todo = this.todos.find(todo => todo.id === json.id);
if (!todo) {
todo = new Todo(this, json.id);
this.todos.push(todo);
}
if (json.isDeleted) {
this.removeTodo(todo);
} else {
todo.updateFromJson(json);
}
}
createTodo() {
var todo = new Todo(this);
this.todos.push(todo);
return todo;
}
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
todo.dispose();
}
}
export class Todo {
id = null;
@observable completed = false;
@observable task = "";
@observable author = null;
store = null;
autoSave = true;
saveHandler = null;
constructor(store, id=uuid.v4()) {
this.store = store;
this.id = id;
this.saveHandler = reaction(
() => this.asJson,
(json) => {
if (this.autoSave) {
this.store.transportLayer.saveTodo(json);
}
}
});
}
delete() {
this.store.transportLayer.deleteTodo(this.id);
this.store.removeTodo(this);
}
@computed get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
};
}
updateFromJson(json) {
this.autoSave = false;
this.completed = json.completed;
this.task = json.task;
this.author = this.store.authorStore.resolveAuthor(json.authorId);
this.autoSave = true;
}
dispose() {
this.saveHandler();
}
}
Writing (async) actions
Writing actions is straight-forward when using MobX. Just create, change or delete data and MobX will make sure that changes are picked up by the store and the components that depend on your data. Based on the store we have created in the previous section, actions become as simple as:
var todo = todoStore.createTodo();
todo.task = "make coffee";
That is enough to create a todo, submit it to the server and update our user interface accordingly.
Writing asynchronous actions is pretty simple as well.
You can use observable data structures as a promise.
This is what happens with the isLoading
property in the todoStore
for example:
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
After completing the asynchronous action, just update your data and your views will update. The render function of a React component could become as simple as:
import {observer} from "mobx-react";
var TodoOverview = observer(function(props) {
var todoStore = props.todoStore;
if (todoStore.isLoading) {
return
Loading...
;
} else {
return
{
todoStore.todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
}

}
});
The above TodoOverview
component will now update whenever isLoading
changes, or when isLoading
is true and the todos
list changes.
Note that we could have expressed todoStore.isLoading
as todoStore.todos.length
as well.
The result would be exactly the same.
Optimizing React components
MobX is very fast, often even faster than Redux. But here are some tips to get most out of React and MobX. Note that most tips apply to React in general and are not specific for MobX.
Use many small components
@observer
components will track all values they use and re-render if any of them changes.
So the smaller your components are, the smaller the change they have to re-render; it means that more parts of your user interface have the possibility to render independently of each other.
Render lists in dedicated components
This is especially true when rendering big collections. React is notoriously bad at rendering large collections as the reconciler has to evaluate the components produced by a collection on each collection change. It is therefore recommended to have components that just map over a collection and render it, and render nothing else:
Bad:
@observer class MyComponent extends Component {
render() {
const {todos, user} = this.props;
return (

{user.name}

{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}

)
}
}
In the above listing React will unnecessarily need to reconcile all TodoView components when the user.name
changes. They won't re-render, but the reconcile process is expensive in itself.
Good:
@observer class MyComponent extends Component {
render() {
const {todos, user} = this.props;
return (

{user.name}
<TodosView todos={todos} />

)
}
}
@observer class TodosView extends Component {
render() {
const {todos} = this.props;
return

{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}

)
}
}
Don't use array indexes as keys
Don't use array indexes or any value that might change in the future as key. Generate id's for your objects if needed. See also this blog.
Dereference values lately
When using mobx-react
it is recommended to dereference values as late as possible.
This is because MobX will re-render components that dereference observable values automatically.
If this happens deeper in your component tree, less components have to re-render.
Fast:
<DisplayName person={person} />
Slower:
<DisplayName name={person.name} />
.
There is nothing wrong to the latter.
But a change in the name
property will, in the first case, trigger the DisplayName
to re-render, while in the latter, the owner of the component has to re-render.
However, it is more important for your components to have a comprehensible API than applying this optimization.
To have the best of both worlds, consider making smaller components:
const PersonNameDisplayer = observer(({ props }) => <DisplayName name={props.person.name} />)
Bind functions early
This tip applies to React in general and libraries using PureRenderMixin
especially, try to avoid creating new closures in render methods.
See also these resources:
Bad:
render() {
return <MyWidget onClick={() => { alert('hi') }} />
}
Good:
render() {
return <MyWidget onClick={this.handleClick} />
}
handleClick = () => {
alert('hi')
}
The bad example will always yield the shouldComponent
of PureRenderMixin
used in MyWidget
to always yield false as you pass a new function each time the parent is re-rendered.
DevTools | MobX
MobX ships with DevTools that can be used to track the rendering behavior and data depenencies of your app.
Install:
npm install mobx-react-devtools
To enable devtools, require the devtools somewhere in your code-base.
require('mobx-react-devtools')
For more details check the mobx-react-devtools repository.
ES5 / ES6 / TypeScript syntax
Here are some examples show you how you can make the state of a simple todo application observable,
using either plain objects or classes.
MobX ships with TypeScript typings in the package (supported in TypeScript 1.6 and higher).
So import * as mobx from "mobx"
gives access to the strongly typed API without further imports.
Creating objects
ES5 / ES6 / TS: Making plain objects observable
var todoStore = mobx.observable({
todos: [{
title: 'Find a clean mug',
completed: true
}],
completedCount: function() {
return this.todos.filter(function(todo) {
return todo.completed;
}).length;
}
});
ES5 constructor functions
If TodoStore
is a constructor function that is typically invoked using the new
keyword,
extendObservable
can be used to add observable properties to the object during creation:
function TodoStore() {
mobx.extendObservable(this, {
todos: [{
title: 'Find a clean mug',
completed: true
}],
completedCount: function() {
return this.todos.filter(function(todo) {
return todo.completed;
}).length;
}
});
}
ES6 / TypeScript classes
When using ES6 or TypeScript it is recommended to use the @observable
decorator to make observable properties and @computed
for derivations.
Note that getters should be used to define observable functions on a class.This is to keep the type of a derived value consistent between ES5 / ES6 and TypeScript:
Use store.completedCount
to obtain a derived value; not store.completedCount()
.
In contrast, @observable someFunction() {}
will just create an observable reference to someFunction
, but someFunction
itself won't become reactive.
class TodoStore {
@observable todos = [{
title: 'Find a clean mug',
completed: true
}];
@computed get completedCount() {
return this.todos.filter((todo) => todo.completed).length;
}
}
React components
In combination with @observer
decorator from the mobx-react
package:
Components in ES5
var MyComponent = observer(React.createClass({
componentWillMount: function() {
mobx.extendObservable(this, {
greeting: "Hello"
});
},
render: function() {
return <button onClick={this.onButtonClick}>{this.greeting}, world</button>
},
onButtonClick: function(e) {
this.greeting = "Hi"
}
});
If stateless component functions are used:
var MyOtherComponent = observer(function(props) {
return
{props.user.name}
;
});
Components in ES6 / TypeScript
@observer class MyComponent extends React.Component {
render() {
return <button onClick={this.onButtonClick}>Hi</button>
}
onButtonClick = (e) => {
// bound function
}
}
If stateless component functions are used:
const MyOtherComponent = observer(props => // ..or with destructuring: ({user}) => ..

{props.user.name{/div}
);
Please note that onButtonClick
is not just a class method, but an instance field which gets a bound function.
This is the most convenient and the fastest way to create bound event handlers.
If {this.onButtonClick.bind(this)}
or {(e) => this.onButtonClick(e)}
was used instead in the rendering, each render invocation would create a new closures.
That is not only slightly slower in itself,
but it will also cause the button
(or any other component) to be always re-rendered because you are effectively passing a new event handler to the button each time MyComponent
is rendered.
Enabling decorators in your transpiler
Decorators are not supported by default when using TypeScript or Babel pending a definitive definition in the ES standard.
- For typescript, enable the
--experimentalDecorators
compiler flag or set the compiler optionexperimentalDecorators
totrue
intsconfig.json
(Recommended) - For babel5, make sure
--stage 0
is passed to the Babel CLI - For babel6, see the example configuration as suggested in this issue. In short, use the transform plugin
transform-decorators-legacy
and make sure it is first in the plugins list!
Stateless Components and Hot Module Reloading
One thing that can be a challenge when getting started with MobX (and React in general) is understanding why Hot Module Reloading (HMR) sometimes breaks. When you initially get it working, it seems like magic (and it kind of is), however there's at least one rough edge with regard to HMR and React: stateless components. Since stateless components don't explicitly identify themselves as React components, HMR doesn't quite know what to do with them, and so you'll often see warnings in your console like this:
[HMR] The following modules couldn't be hot updated: (Full reload needed)
This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves. See http://webpack.github.io/docs/hot-module-replacement-with-webpack.html for more details.
[HMR] - ./src/ToDoItem.jsx
This is especially apparent when you start working with MobX since observables make it really easy to create a lot of stateless components. Here are some tips for how to build your stateless components and still get all the advantages of HMR:
Use function declarations instead of arrow functions
Function declarations still do the exact same thing as arrow functions, but they have the key advantage of actually having names inside of the React DevTools.
For instance, here's a stateless component built with an arrow function:
const ToDoItem = observer(props =>
{props.item}
);
export default ToDoItem;
And here's how that will appear in the React DevTools:
On the other hand, using a function declaration will allow you to build the same stateless component AND see it in the DevTools:
function ToDoItem(props) {
return
{props.item}

}
export default observer(ToDoItem);
And now the component shows up correctly in the DevTools:
Make sure your top-level component is a stateful observer
By "stateful observer", all I really mean is a component created with React.Component
or React.createClass
and which uses the @observer
decorator, like so:
import { observer } from 'mobx-react';
@observer
class App extends React.Component {
constructor(props) {
super(props);
this.store = props.store;
}
render() {
return (

Todos:

{
this.store.todos.map((t, idx) => <ToDoItem key={idx} item={t}/>)
}

);
}
}
In this case, ToDoItem
is stateless, but will still work with HMR because the root-level of the UI tree is a stateful observer. As a result, whenever we change any stateless component, it will be hot-reloaded because the observers will trigger computations in the root-level component as well. And since the root-level component is a good old-fashioned React component, it'll trigger the HMR for all of its children and voila! All the magic of stateless components, observables, and hot module reloading working together beautifully.
Common Pitfalls & Best Practices
Stuck with MobX? This section contains a list of common issues people new to MobX might run into.
Array.isArray(observable([1,2,3])) === false
In ES5 there is no way to reliably inherit from arrays, and hence observable arrays inherit from objects.
This means that regularly libraries are not able to recognize observable arrays as normal arrays (like lodash, or built-in operations like Array.concat
).
This can simply be fixed by passing calling observable.toJS()
or observable.slice()
before passing the array to another library.
As long as the external library has no intent to modify the array, this will further work completely as expected.
You can use isObservableArray(observable)
to check whether something is an observable array.
object.someNewProp = value
is not picked up
MobX observable objects do not detect or react to property assignments that weren't declared observable before.
So MobX observable objects act as records with predefined keys.
You can use extendObservable(target, props)
to introduce new observable properties to an object.
However object iterators like for .. in
or Object.keys()
won't react to this automatically.
If you need a dynamically keyed object, for example to store users by id, create observable _map_s using asMap
.
More info on asMap.
For more info see what will MobX react to?.
Use @observer
on all components that render @observable
's.
@observer
only enhances the component you are decorating, not the components used inside it.
So usually all your components should be decorated. Don't worry, this is not inefficient, in contrast, more observer
components make rendering more efficient.
Dereference values as lately as possible
MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object see boxed observables).
So it is not the values that are observable, but the properties of an object. This means that @observer
actually reacts to the fact that you dereference a value.
So in our above example, the Timer
component would not react if it was initialized as follows:
React.render(<Timer timerData={timerData.secondsPassed} />, document.body)
In this snippet just the current value of secondsPassed
is passed to the Timer
, which is the immutable value 0
(all primitives are immutable in JS).
That number won't change anymore in the future, so Timer
will never update. It is the property secondsPassed
that will change in the future,
so we need to access it in the component. Or in other words: always try to pass the owning object of an observable property.
For more info see what will MobX react to?.
Computed values run more often then expected
If a computed property is not in use by some reaction (autorun
, observer
etc), computed expressions will be evaluated lazily; each time their value is requested (so they just act as normal property).
Computed values will only track their dependencies if they are observed.
This allows MobX to automatically suspend computations that are not actively in use.
See this blog or issue #356 for an explanation.
So if you fiddle arounds, computed properties might not seem efficient. But when applied in a project that uses observer
, autorun
etc, they become very efficient.
N.B. in a next version of MobX computeds will automatically be kept alive during transactions as well, see PR: #452
Always dispose reactions
all forms of autorun
, observe
and intercept
will only be garbage collected if all objects they observe are garbage collection themselves.
So it is recommend to use the disposer function that is returned from these methods to stop them when you no longer need them.
Usually for observe
and intercept
it is not strictly necessary to dispose them if when targed this
.
For reactions like autorun
it is more tricky, as they might observe many different observables, and as long as one of them is still in scope,
the reaction will remain in scope which means that all other observables it uses are also kept alive to support future recomputions.
So make sure to always dispose your reactions when you no longer need them!
Example:
const VAT = observable(1.20)
class OrderLIne {
@observable price = 10;
@observable amount = 1;
constructor() {
this.handler = autorun(() => {
doSomethingWith(this.price * this.amount)
})
this.handler = autorun(() => {
doSomethingWith(this.price * this.amount * VAT.get())
})
this.handler()
}
}
I have a weird exception when using @observable
in a React component.
The following exception: Uncaught TypeError: Cannot assign to read only property '__mobxLazyInitializers' of object
occurs when using a react-hot-loader
that does not support decorators.
Either use extendObservable
in componentWillMount
instead of @observable
, or upgrade to react-hot-loader
"^3.0.0-beta.2"
or higher.
The display name of react components is not set
If you use export const MyComponent = observer((props =>
hi
))
, no display name will be visible in the devtools.
The following approaches can be used to fix this:
export const MyComponent = observer((props =>
hi
))
myComponent.displayName = "MyComponent"
export const MyComponent = observer(function MyComponent(props) { return
hi
 })
// 3 (transpiler will infer component name from variable name)
const _MyComponent = observer((props =>
hi
))
export const MyComponent = observer(_MyComponent)
const MyComponent = observer((props =>
hi
))
export default observer(MyComponent)
See also: http://mobxjs.github.io/mobx/best/stateless-HMR.html or #141.
The propType of an observable array is object
Observable arrays are actually objects, so they comply to propTypes.object
instead of array
.
mobx-react
will soon provide explicit propTypes for observable data structures.
Declaring propTypes might cause unnecessary renders in dev mode
See: https://github.com/mobxjs/mobx-react/issues/56
@observable
properties initialize lazily when using Babel
This issue only occurs when transpiling with Babel and not with Typescript (in which decorator support is more mature). Observable properties will not be instantiated upon an instance until the first read / write to a property (at that point they all will be initialized). This results in the following subtle bug:
class Todo {
@observable done = true
@observable title = "test"
}
const todo = new Todo()
"done" in todo
todo.hasOwnProperty("done")
Object.keys(todo)
console.log(todo.title)
"done" in todo
todo.hasOwnProperty("done")
Object.keys(todo)
In practice this is rarely an issue, only when using generic methods like Object.assign(target, todo)
or assert.deepEquals
before reading or writing any property of the object.
If you want to make sure that this issue doesn't occur, just initialize the fields in the constructor instead of at the field declaration or use extendObservable
to create the observable properties.
Defining data stores
Best Practices for building large scale maintainable projects
This section contains some best practices we discovered at Mendix while working with MobX. This section is opinionated and you are in no way forced to apply these practices. There are many ways of working with MobX and React, and this is just one of them.
Stores
Let's start with stores. In the next sections we will discuss actions and React components as well. Stores can be found in any Flux architecture and can be compared a bit with controllers in the MVC pattern. The main responsibility of stores is to move logic and state out of your components into a standalone testable unit that can be used in both frontend and backend JavaScript.
Stores for the user interface state
Most applications benefit from having at least two stores. One for the UI state and one or more for the domain state. The advantage of separating those two is you can reuse and test domain state universally, and you might very well reuse it in other applications. The ui-state-store however is often very specific for your application. But usually very simple as well. This store typically doesn't have much logic in it, but will store a plethora of loosely coupled pieces of information about the UI. This is ideal as most applications will change the UI state often during the development process.
Things you will typically find in UI stores:
- Session information
- Information about how far your application has loaded
- Information that will not be stored in the backend
- Information that affects the UI globally
- Window dimensions
- Accessibility information
- Current language
- Currently active theme
- User interface state as soon as it effects multiple, further unrelated components:
- Current selection
- Visibility of toolbars, etc.
- State of a wizard
- State of a global overlay
It might very well be that these pieces of information start as internal state of a specific component (for example the visibility of a toolbar). But after a while you discover that you need this information somewhere else in your application. Instead of pushing state in such a case upwards in the component tree, like you would do in plain React apps, you just move that state to the ui-state-store.
Make sure this state is a singleton. For isomorphic applications you might also want to provide a stub implementation of this store with sane defaults so that all components render as expected. You might distribute the ui-state-store through your application by passing it as a property through your component tree. You can also pass this store by using context or make it globally available as a module. For testing purposes, I recommend to just pass it through the component tree.
Example of a store (using ES6 syntax):
import {observable, computed, asStructure} from 'mobx';
import jquery from 'jquery';
class UiState {
@observable language = "en_US";
@observable pendingRequestCount = 0;
@observable windowDimensions = asStructure({
width: jquery(window).width(),
height: jquery(window).height()
});
constructor() {
jquery.resize(() => {
this.windowDimensions = getWindowDimensions();
});
}
@computed get appIsInSync() {
return this.pendingRequestCount === 0
}
}
singleton = new UiState();
export default singleton;
Domain Stores
Your application will contain one or multiple domain stores. These stores store the data your application is all about. Todo items, users, books, movies, orders, you name it. Your application will most probably have at least one domain store.
A single domain store should be responsible for a single concept in your application. However a single concept might take the form of multiple subtypes and it is often a (cyclic) tree structure. For example: one domain store for your products, and one for our orders and orderlines. As a rule of thumb: if the nature of the relationship between two items is containment, they should typically be in the same store. So a store just manages domain objects.
These are the responsibility of a store:
- Instantiate domain objects. Make sure domain objects know the store they belong to.
- Make sure there is only one instance of each of your domain objects. The same user, order or todo should not be twice in your memory. This way you can safely use references and also be sure you are looking at the latest instance, without ever having to resolve a reference. This is fast, straightforward and convenient when debugging.
- Provide backend integration. Store data when needed.
- Update existing instances if updates are received from the backend.
- Provide a stand-alone, universal, testable component of your application.
- To make sure your store is testable and can be run server-side, you probably will move doing actual websocket / http requests to a separate object so that you can abstract over your communication layer.
- There should be only one instance of a store.
Domain objects
Each domain object should be expressed using its own class (or constructor function). It is recommended to store your data in denormalized form. There is no need to treat your client-side application state as some kind of database. Real references, cyclic data structures and instance methods are powerful concepts in JavaScript. Domain objects are allowed to refer directly to domain objects from other stores. Remember: we want to keep our actions and views as simple as possible and needing to manage references and doing garbage collection yourself might be a step backward. Unlike many Flux architectures, with MobX there is no need to normalize your data, and this makes it a lot simpler to build the essentially complex parts of your application: your business rules, actions and user interface.
Domain objects can delegate all their logic to the store they belong to if that suits your application well. It is possible to express your domain objects as plain objects, but classes have some important advantages over plain objects:
- They can have methods. This makes your domain concepts easier to use stand-alone and reduces the amount of contextual awareness that is needed in your application. Just pass objects around. You don't have to pass stores around, or have to figure out which actions can be applied to an object if they are just available as instance methods. Especially in large applications this is important.
- They offer fine grained control over the visibility of attributes and methods.
- Objects created using a constructor function can freely mix observable properties and functions, and non-observable properties and methods.
- They are easily recognizable and can strictly be type-checked.
Example domain store
import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';
export class TodoStore {
authorStore;
transportLayer;
@observable todos = [];
@observable isLoading = true;
constructor(transportLayer, authorStore) {
this.authorStore = authorStore;
this.transportLayer = transportLayer;
this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
this.loadTodos();
}
loadTodos() {
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
}
updateTodoFromServer(json) {
var todo = this.todos.find(todo => todo.id === json.id);
if (!todo) {
todo = new Todo(this, json.id);
this.todos.push(todo);
}
if (json.isDeleted) {
this.removeTodo(todo);
} else {
todo.updateFromJson(json);
}
}
createTodo() {
var todo = new Todo(this);
this.todos.push(todo);
return todo;
}
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
todo.dispose();
}
}
export class Todo {
id = null;
@observable completed = false;
@observable task = "";
@observable author = null;
store = null;
autoSave = true;
saveHandler = null;
constructor(store, id=uuid.v4()) {
this.store = store;
this.id = id;
this.saveHandler = reaction(
() => this.asJson,
(json) => {
if (this.autoSave) {
this.store.transportLayer.saveTodo(json);
}
}
});
}
delete() {
this.store.transportLayer.deleteTodo(this.id);
this.store.removeTodo(this);
}
@computed get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
};
}
updateFromJson(json) {
this.autoSave = false;
this.completed = json.completed;
this.task = json.task;
this.author = this.store.authorStore.resolveAuthor(json.authorId);
this.autoSave = true;
}
dispose() {
this.saveHandler();
}
}
Writing (async) actions
Writing actions is straight-forward when using MobX. Just create, change or delete data and MobX will make sure that changes are picked up by the store and the components that depend on your data. Based on the store we have created in the previous section, actions become as simple as:
var todo = todoStore.createTodo();
todo.task = "make coffee";
That is enough to create a todo, submit it to the server and update our user interface accordingly.
Writing asynchronous actions is pretty simple as well.
You can use observable data structures as a promise.
This is what happens with the isLoading
property in the todoStore
for example:
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
After completing the asynchronous action, just update your data and your views will update. The render function of a React component could become as simple as:
import {observer} from "mobx-react";
var TodoOverview = observer(function(props) {
var todoStore = props.todoStore;
if (todoStore.isLoading) {
return
Loading...
;
} else {
return
{
todoStore.todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
}

}
});
The above TodoOverview
component will now update whenever isLoading
changes, or when isLoading
is true and the todos
list changes.
Note that we could have expressed todoStore.isLoading
as todoStore.todos.length
as well.
The result would be exactly the same.
Optimizing React components
MobX is very fast, often even faster than Redux. But here are some tips to get most out of React and MobX. Note that most tips apply to React in general and are not specific for MobX.
Use many small components
@observer
components will track all values they use and re-render if any of them changes.
So the smaller your components are, the smaller the change they have to re-render; it means that more parts of your user interface have the possibility to render independently of each other.
Render lists in dedicated components
This is especially true when rendering big collections. React is notoriously bad at rendering large collections as the reconciler has to evaluate the components produced by a collection on each collection change. It is therefore recommended to have components that just map over a collection and render it, and render nothing else:
Bad:
@observer class MyComponent extends Component {
render() {
const {todos, user} = this.props;
return (

{user.name}

{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}

)
}
}
In the above listing React will unnecessarily need to reconcile all TodoView components when the user.name
changes. They won't re-render, but the reconcile process is expensive in itself.
Good:
@observer class MyComponent extends Component {
render() {
const {todos, user} = this.props;
return (

{user.name}
<TodosView todos={todos} />

)
}
}
@observer class TodosView extends Component {
render() {
const {todos} = this.props;
return

{todos.map(todo => <TodoView todo={todo} key={todo.id} />)}

)
}
}
Don't use array indexes as keys
Don't use array indexes or any value that might change in the future as key. Generate id's for your objects if needed. See also this blog.
Dereference values lately
When using mobx-react
it is recommended to dereference values as late as possible.
This is because MobX will re-render components that dereference observable values automatically.
If this happens deeper in your component tree, less components have to re-render.
Fast:
<DisplayName person={person} />
Slower:
<DisplayName name={person.name} />
.
There is nothing wrong to the latter.
But a change in the name
property will, in the first case, trigger the DisplayName
to re-render, while in the latter, the owner of the component has to re-render.
However, it is more important for your components to have a comprehensible API than applying this optimization.
To have the best of both worlds, consider making smaller components:
const PersonNameDisplayer = observer(({ props }) => <DisplayName name={props.person.name} />)
Bind functions early
This tip applies to React in general and libraries using PureRenderMixin
especially, try to avoid creating new closures in render methods.
See also these resources:
Bad:
render() {
return <MyWidget onClick={() => { alert('hi') }} />
}
Good:
render() {
return <MyWidget onClick={this.handleClick} />
}
handleClick = () => {
alert('hi')
}
The bad example will always yield the shouldComponent
of PureRenderMixin
used in MyWidget
to always yield false as you pass a new function each time the parent is re-rendered.
DevTools | MobX
MobX ships with DevTools that can be used to track the rendering behavior and data depenencies of your app.
Install:
npm install mobx-react-devtools
To enable devtools, require the devtools somewhere in your code-base.
require('mobx-react-devtools')
For more details check the mobx-react-devtools repository.
ES5 / ES6 / TypeScript syntax
Here are some examples show you how you can make the state of a simple todo application observable,
using either plain objects or classes.
MobX ships with TypeScript typings in the package (supported in TypeScript 1.6 and higher).
So import * as mobx from "mobx"
gives access to the strongly typed API without further imports.
Creating objects
ES5 / ES6 / TS: Making plain objects observable
var todoStore = mobx.observable({
todos: [{
title: 'Find a clean mug',
completed: true
}],
completedCount: function() {
return this.todos.filter(function(todo) {
return todo.completed;
}).length;
}
});
ES5 constructor functions
If TodoStore
is a constructor function that is typically invoked using the new
keyword,
extendObservable
can be used to add observable properties to the object during creation:
function TodoStore() {
mobx.extendObservable(this, {
todos: [{
title: 'Find a clean mug',
completed: true
}],
completedCount: function() {
return this.todos.filter(function(todo) {
return todo.completed;
}).length;
}
});
}
ES6 / TypeScript classes
When using ES6 or TypeScript it is recommended to use the @observable
decorator to make observable properties and @computed
for derivations.
Note that getters should be used to define observable functions on a class.This is to keep the type of a derived value consistent between ES5 / ES6 and TypeScript:
Use store.completedCount
to obtain a derived value; not store.completedCount()
.
In contrast, @observable someFunction() {}
will just create an observable reference to someFunction
, but someFunction
itself won't become reactive.
class TodoStore {
@observable todos = [{
title: 'Find a clean mug',
completed: true
}];
@computed get completedCount() {
return this.todos.filter((todo) => todo.completed).length;
}
}
React components
In combination with @observer
decorator from the mobx-react
package:
Components in ES5
var MyComponent = observer(React.createClass({
componentWillMount: function() {
mobx.extendObservable(this, {
greeting: "Hello"
});
},
render: function() {
return <button onClick={this.onButtonClick}>{this.greeting}, world</button>
},
onButtonClick: function(e) {
this.greeting = "Hi"
}
});
If stateless component functions are used:
var MyOtherComponent = observer(function(props) {
return
{props.user.name}
;
});
Components in ES6 / TypeScript
@observer class MyComponent extends React.Component {
render() {
return <button onClick={this.onButtonClick}>Hi</button>
}
onButtonClick = (e) => {
// bound function
}
}
If stateless component functions are used:
const MyOtherComponent = observer(props => // ..or with destructuring: ({user}) => ..

{props.user.name{/div}
);
Please note that onButtonClick
is not just a class method, but an instance field which gets a bound function.
This is the most convenient and the fastest way to create bound event handlers.
If {this.onButtonClick.bind(this)}
or {(e) => this.onButtonClick(e)}
was used instead in the rendering, each render invocation would create a new closures.
That is not only slightly slower in itself,
but it will also cause the button
(or any other component) to be always re-rendered because you are effectively passing a new event handler to the button each time MyComponent
is rendered.
Enabling decorators in your transpiler
Decorators are not supported by default when using TypeScript or Babel pending a definitive definition in the ES standard.
- For typescript, enable the
--experimentalDecorators
compiler flag or set the compiler optionexperimentalDecorators
totrue
intsconfig.json
(Recommended) - For babel5, make sure
--stage 0
is passed to the Babel CLI - For babel6, see the example configuration as suggested in this issue. In short, use the transform plugin
transform-decorators-legacy
and make sure it is first in the plugins list!
Stateless Components and Hot Module Reloading
One thing that can be a challenge when getting started with MobX (and React in general) is understanding why Hot Module Reloading (HMR) sometimes breaks. When you initially get it working, it seems like magic (and it kind of is), however there's at least one rough edge with regard to HMR and React: stateless components. Since stateless components don't explicitly identify themselves as React components, HMR doesn't quite know what to do with them, and so you'll often see warnings in your console like this:
[HMR] The following modules couldn't be hot updated: (Full reload needed)
This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves. See http://webpack.github.io/docs/hot-module-replacement-with-webpack.html for more details.
[HMR] - ./src/ToDoItem.jsx
This is especially apparent when you start working with MobX since observables make it really easy to create a lot of stateless components. Here are some tips for how to build your stateless components and still get all the advantages of HMR:
Use function declarations instead of arrow functions
Function declarations still do the exact same thing as arrow functions, but they have the key advantage of actually having names inside of the React DevTools.
For instance, here's a stateless component built with an arrow function:
const ToDoItem = observer(props =>
{props.item}
);
export default ToDoItem;
And here's how that will appear in the React DevTools:
On the other hand, using a function declaration will allow you to build the same stateless component AND see it in the DevTools:
function ToDoItem(props) {
return
{props.item}

}
export default observer(ToDoItem);
And now the component shows up correctly in the DevTools:
Make sure your top-level component is a stateful observer
By "stateful observer", all I really mean is a component created with React.Component
or React.createClass
and which uses the @observer
decorator, like so:
import { observer } from 'mobx-react';
@observer
class App extends React.Component {
constructor(props) {
super(props);
this.store = props.store;
}
render() {
return (

Todos:

{
this.store.todos.map((t, idx) => <ToDoItem key={idx} item={t}/>)
}

);
}
}
In this case, ToDoItem
is stateless, but will still work with HMR because the root-level of the UI tree is a stateful observer. As a result, whenever we change any stateless component, it will be hot-reloaded because the observers will trigger computations in the root-level component as well. And since the root-level component is a good old-fashioned React component, it'll trigger the HMR for all of its children and voila! All the magic of stateless components, observables, and hot module reloading working together beautifully.
7. Utility functions
autorunAsync
autorunAsync(action: () => void, minimumDelay?: number, scope?)
Just like autorun
except that the action won't be invoked synchronously but asynchronously after the minimum amount of milliseconds has passed.
The action
will be run and observed.
However, instead of running the action immediately when the values it observes have changed, the minimumDelay
will be awaited before re-execution the action
again.
If observed values are changed multiple times while waiting, the action is still triggered only once, so in a sense it achieves a similar effect than a transaction.
This might be useful for stuff that is expensive and doesn't need to happen synchronously; such as debouncing server communication.
If a string is passed as first argument to autorun
, it will be used as debug name.
autorunAsync
returns a disposer to cancel the autorun.
autorunAsync(() => {
sendProfileToServer(profile.asJson);
}, 300);
Atom & Reaction
Atoms
At some point you might want to have more data structures or other things (like streams) that can be used in reactive computations.
Achieving that is pretty simple by using the Atom
class.
Atoms can be used to signal MobX that some observable data source has been observed or changed.
And MobX will signal the atom whenever it is used or no longer in use.
The following example demonstrates how you can create an observable Clock
, which can be used in reactive functions,
and returns the current date-time.
This clock will only actually tick if it is observed by someone.
The complete API of the Atom
class is demonstrated by this example.
import {Atom, autorun} from "mobx";
class Clock {
atom;
intervalHandler = null;
currentDateTime;
constructor() {
this.atom = new Atom(
"Clock",
() => this.startTicking(),
() => this.stopTicking()
);
}
getTime() {
if (this.atom.reportObserved()) {
return this.currentDateTime;
} else {
return new Date();
}
}
tick() {
this.currentDateTime = new Date();
this.atom.reportChanged();
}
startTicking() {
this.tick();
this.intervalHandler = setInterval(
() => this.tick(),
1000
);
}
stopTicking() {
clearInterval(this.intervalHandler);
this.intervalHandler = null;
}
}
const clock = new Clock();
const disposer = autorun(() => console.log(clock.getTime()));
disposer();
Reactions
Reaction
allows you to create your own 'auto runner'.
Reactions track a function and signal when the function should be executed again because one or more dependencies have changed.
This is how autorun
is defined using Reaction
:
export function autorun(view: Lambda, scope?: any) {
if (scope)
view = view.bind(scope);
const reaction = new Reaction(view.name || "Autorun", function () {
this.track(view);
});
if (isComputingDerivation() || globalState.inTransaction > 0)
globalState.pendingReactions.push(reaction);
else
reaction.runReaction();
return reaction.getDisposer();
}
createTransformer
createTransformer(transformation: (value: A) => B, onCleanup?: (result: B, value?: A) => void): (value: A) => B
createTransformer
turns a function (that should transform one value into another value) into a reactive and memoizing function.
In other words, if the transformation
function computes B given a specific A, the same B will be returned for all other future invocations of the transformation with the same A.
However, if A changes, the transformation will be re-applied so that B is updated accordingly.
And last but not least, if nobody is using the transformation of a specific A anymore, it's entry will be removed from the memoization table.
With createTransformer
it is very easy to transform a complete data graph into another data graph.
Transformation functions can be composed so that you can build a tree using lots of small transformations.
The resulting data graph will never be stale, it will be kept in sync with the source by applying small patches to the result graph.
This makes it very easy to achieve powerful patterns similar to sideways data loading, map-reduce, tracking state history using immutable data structures etc.
The optional onCleanup
function can be used to get a notification when a transformation of an object is no longer needed.
This can be used to dispose resources attached to the result object if needed.
Always use transformations inside a reaction like @observer
or autorun
.
Transformations will, like any other computed value, fall back to lazy evaluation if not observed by something, which sort of defeats their purpose.
This all might still be a bit vague, so here are two examples that explain this whole idea of transforming one data structure into another by using small, reactive functions:
Tracking mutable state using immutable, shared data structures.
This example is taken from the Reactive2015 conference demo:
const store = observable({
boxes: [],
arrows: [],
selection: null
});
const states = [];
autorun(() => {
states.push(serializeState(store));
});
const serializeState = createTransformer(store => ({
boxes: store.boxes.map(serializeBox),
arrows: store.arrows.map(serializeArrow),
selection: store.selection ? store.selection.id : null
}));
const serializeBox = createTransformer(box => ({...box}));
const serializeArrow = createTransformer(arrow => ({
id: arrow.id,
to: arrow.to.id,
from: arrow.from.id
}));
In this example the state is serialized by composing three different transformation functions.
The autorunner triggers the serialization of the store
object, which in turn serializes all boxes and arrows.
Let's take closer look at the life of an imaginary example box#3.
- The first time box#3 is passed by
map
toserializeBox
, the serializeBox transformation is executed and an entry containing box#3 and its serialized representation is added to the internal memoization table ofserializeBox
. - Imagine that another box is added to the
store.boxes
list. This would cause theserializeState
function to re-compute, resulting in a complete remapping of all the boxes. However, all the invocations ofserializeBox
will now return their old values from the memoization tables since their transformation functions didn't (need to) run again. - Secondly, if somebody changes a property of box#3 this will cause the application of the
serializeBox
to box#3 to re-compute, just like any other reactive function in MobX. Since the transformation will now produce a new Json object based on box#3, all observers of that specific transformation will be forced to run again as well. That's theserializeState
transformation in this case.serializeState
will now produce a new value in turn and map all the boxes again. But except for box#3, all other boxes will be returned from the memoization table. - Finally, if box#3 is removed from
store.boxes
,serializeState
will compute again. But since it will no longer be using the application ofserializeBox
to box#3, that reactive function will go back to non-reactive mode. This signals the memoization table that the entry can be removed so that it is ready for GC.
So effectively we have achieved state tracking using immutable, shared datas structures here.
All boxes and arrows are mapped and reduced into single state tree.
Each change will result in a new entry in the states
array, but the different entries will share almost all of their box and arrow representations.
Transforming a datagraph into another reactive data graph
Instead of returning plain values from a transformation function, it is also possible to return observable objects. This can be used to transform an observable data graph into a another observable data graph, which can be used to transform... you get the idea.
Here is a small example that encodes a reactive file explorer that will update its representation upon each change. Data graphs that are built this way will in general react a lot faster and will consist of much more straight-forward code, compared to derived data graph that are updated using your own code. See the performance tests for some examples.
Unlike the previous example, the transformFolder
will only run once as long as a folder remains visible;
the DisplayFolder
objects track the associated Folder
objects themselves.
In the following example all mutations to the state
graph will be processed automatically.
Some examples:
- Changing the name of a folder will update it's own
path
property and thepath
property of all its descendants. - Collapsing a folder will remove all descendant
DisplayFolders
from the tree. - Expanding a folder will restore them again.
- Setting a search filter will remove all nodes that do not match the filter, unless they have a descendant that matches the filter.
- Etc.
function Folder(parent, name) {
this.parent = parent;
m.extendObservable(this, {
name: name,
children: m.asFlat([]),
});
}
function DisplayFolder(folder, state) {
this.state = state;
this.folder = folder;
m.extendObservable(this, {
collapsed: false,
name: function() {
return this.folder.name;
},
isVisible: function() {
return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible);
},
children: function() {
if (this.collapsed)
return [];
return this.folder.children.map(transformFolder).filter(function(child) {
return child.isVisible;
})
},
path: function() {
return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name;
}
});
}
var state = m.observable({
root: new Folder(null, "root"),
filter: null,
displayRoot: null
});
var transformFolder = m.createTransformer(function (folder) {
return new DisplayFolder(folder, state);
});
m.autorun(function() {
state.displayRoot = transformFolder(state.root);
});
expr | MobX
expr
can be used to create temporarily computed values inside computed values.
Nesting computed values is useful to create cheap computations to prevent expensive computations to run.
In the following example the expression prevents that the TodoView
component is re-rendered if the selection changes elsewhere.
Instead the component will only re-render when the relevant todo is (de)selected.
const TodoView = observer(({todo, editorState}) => {
const isSelected = mobx.expr(() => editorState.selection === todo);
return
{todo.title}
;
});
expr(func)
is an alias for computed(func).get()
.
extendObservable
Quite similar to Object.assign
, extendObservable
takes two or more arguments, a target
object and one or more properties
maps.
It adds all key-value pairs from the properties to the target
as observable properties.
If an argumentless function is passed as value of a property, extendObservable
will introduce a computed
property instead of an observable property.
var Person = function(firstName, lastName) {
extendObservable(this, {
firstName: firstName,
lastName: lastName
});
}
var matthew = new Person("Matthew", "Henry");
extendObservable(matthew, {
age: 353
});
(N.b: observable(object)
is actually an alias for extendObservable(object, object)
)
isObservable
isObservable
Returns true if the given value was made observable by MobX. Optionally accepts a second string parameter to see whether a specific property is observable.
var person = observable({
firstName: "Sherlock",
lastName: "Holmes"
});
person.age = 3;
isObservable(person);
isObservable(person, "firstName");
isObservable(person.firstName);
isObservable(person, "age");
isObservableMap
Returns true if the given object is created using mobx.map
.
isObservableArray
Returns true if the given object is an array that was made observable using mobx.observable(array)
.
isObservableObject
Returns true if the given object is an object that was made observable using mobx.observable(object)
.
modifiers | MobX
By default, observable
recursively makes all the values of plain objects and arrays recursively observable.
Besides that, it automatically converts functions without arguments into reactive views or derived properties.
For all other types of values just a reference is stored.
In general, this should just do what you need, but if you want you can override the default behavior using modifiers.
Note that modifiers are 'sticky', they are interpreted as being annotations.
They do not only apply to the current value, but also to all values that are assigned in the future to the same attribute.
Note that the attributes class instances (all objects that have a prototype) will not be made observable automatically by observable
.
It is considered to be the responsibility of the class definition / constructor function to mark the necessary attributes of a class instance observable / computed.
asMap
Creates a new Observable Map instead of an Observable Object. See asMap
asReference
The most common modifier is asReference
.
If this modifier is used, observable
will not attempt to make the value observable.
Use this for example if you want to store a reference to a function, instead of creating a view based on that function.
You can also use it to prevent that plain objects or arrays are made observable automatically.
var test = observable({
x : 3,
doubler: function() {
return this.x*2;
},
someFunc: asReference(function() {
return this.x;
})
});
console.log(test.doubler);
console.log(test.someFunc);
asStructure
Can be used on non-cyclic, plain JavaScript values.
Instead of comparing old values with new values based on whether the reference has changed, values are compared using deep equality before notifying any observers.
This is useful if you are working with 'struct' like objects like colors or coordinates and each time return fresh objects with possibly the same values.
asStructure
can be used on reactive functions, plain objects and arrays.
var ViewPort = mobxReact.observer(React.createClass({
displayName: 'ViewPort',
componentWillMount: function() {
mobx.extendObservable({
screenSize: {
width: 0,
height: 0
},
minSize: {
width: 400,
height: 300
},
viewPortSize: mobx.asStructure(function() {
return {
width: Math.max(screenSize.width, minSize.width),
height: Math.max(screenSize.height, minSize.height)
}
}
});
window.onresize = function() {
mobx.transaction(function() {
this.screenSize.width = window.clientWidth;
this.screenSize.height = window.clientHeight;
});
}.bind(this);
},
render: function() {
return (

test

);
}
}));
In the above example, the computed method viewPortSize
returns a fresh object on each re-computation.
So MobX considers it to have changed always. This means that each resize
event of the browser will trigger a re-render of the
ViewPort
component.
However, if the window size is smaller that the minSize
, the resize doesn't need to influence the rendering anymore, as the computed
will return the same dimensions after each run. asStructure
signals to MobX that observers of this computation should only be triggered
if the value returned by the computed has structurally changed (by default strict equality is used to determine whether observers need to be notified).
This means that a new object that is returned from viewPortSize
won't trigger a render
if its contents are (structurally) the same as the previous value.
To use the asStructure
modifier in combination with the @computed
decorator, use the following:
@computed({ asStructure: true }) get viewPortSize() {
return {
width: Math.max(screenSize.width, minSize.width),
height: Math.max(screenSize.height, minSize.height)
}
}
asFlat
Similar to asReference
, except that asFlat
does not prevent its value from becoming observable, but only the children of the value.
It can be used for example to create an observable array or object that should not automatically make its children observable.
var todos = observable(asFlat([{
title: "make coffee",
completed: false
}]));
isObservable(todos);
isObservable(todos[0]);
isObservable(todos[0], "title");
observe | MobX
observe
and intercept
can be used to monitor the changes of a single observable (they don't track nested observables).
intercept
can be used to detect and modify mutations before they are applied to the observable.
observe
allows you to intercept changes after they have been made.
Intercept
Usage: intercept(target, propertyName?, interceptor)
target
: the observable to guardpropertyName
: optional parameter to specify a specific property to intercept. Note thatintercept(user.name, interceptor)
is fundamentally different fromintercept(user, "name", interceptor)
. The first tries to add an interceptor to the currentvalue
insideuser.name
(which might not be an observable at all), the latter intercepts changes to thename
property ofuser
.interceptor
: callback that will be invoked for each change that is made to the observable. Receives a single change object describing the mutation.
The intercept
should tell MobX what needs to happen with the current change.
Therefore it should do one of the following things:
- Return the received
change
object as-is from the function, in wich case the mutation will be applied. - Modify the
change
object and return it, for example to normalize the data. Not all fields are modifiable, see below. - Return
null
, this indicates that the change can be ignored and shouldn't be applied. This is a powerful concept to make your objects for example temporarily immutable. - Throw an exception, for example if some invariant isn't met.
The function returns a disposer
function that can be used to cancel the interceptor.
It is possible to register multiple interceptors to the same observable.
They will be chained in registration order.
If one of the interceptors returns null
or throw an exception, the other interceptors won't be evaluated anymore.
It is also possible to register an interceptor both on a parent object and on an individual property.
In that case the parent object interceptors are run before the property interceptors.
const theme = observable({
backgroundColor: "#ffffff"
})
intercept(theme, "backgroundColor", change => {
if (!change.newValue) {
return null;
}
if (change.newValue.length === 6) {
change.newValue = '#' + change.newValue;
return change;
}
if (change.newValue.length === 7) {
return change;
}
throw new Error("This doesn't like a color at all: " + change.newValue);
})
Observe
Usage: observe(target, propertyName?, listener, invokeImmediately?)
target
: the observable to observepropertyName
: optional parameter to specify a specific property to observe. Note thatobserve(user.name, listener)
is fundamentally different fromobserve(user, "name", listener)
. The first observes the currentvalue
insideuser.name
(which might not be an observable at all), the latter observes thename
property ofuser
.listener
: callback that will be invoked for each change that is made to the observable. Receives a single change object describing the mutation, except for boxed observables, which will invoke thelistener
two parameters:newValue, oldValue
.invokeImmediately
: by default false. Set it to true if you wantobserve
to invokelistener
directly with the state of the observable (instead of waiting for the first change). Not supported (yet) by all kinds of observables.
The function returns a disposer
function that can be used to cancel the observer.
Note that transaction
does not affect the working of the observe
method(s).
This means that even inside a transaction observe
will fire its listeners for each mutation.
Hence autorun
is usually a more powerful and declarative alternative to observe
.
Example:
import {observable, observe} from 'mobx';
const person = observable({
firstName: "Maarten",
lastName: "Luther"
});
const disposer = observe(person, (change) => {
console.log(change.type, change.name, "from", change.oldValue, "to", change.object[change.name]);
});
person.firstName = "Martin";
disposer();
const disposer2 = observe(person, "lastName", (newValue, oldValue) => {
console.log("LastName changed to ", newValue);
});
Related blog: Object.observe is dead. Long live mobx.observe
Event overview
The callbacks of intercept
and observe
will receive an event object which has at least the following properties:
object
: the observable triggering the eventtype
: (string) the type of the current event
These are the additional fields that are available per type:
| observable type | event type | property | description | available during intercept | can be modified by intercept |
|---|---|---|---|---|---|
| Object | add | name | name of the property being added | √ | |
| newValue | the new value being assigned | √ | √ | ||
| update | name | name of the property being updated | √ | ||
| newValue | the new value being assigned | √ | √ | ||
| oldValue | the value that is replaced | ||||
| Array | splice | index | starting index of the splice. Splices are also fired by push , unshift , replace etc. |
√ | |
| removedCount | amount of items being removed | √ | √ | ||
| added | array with items being added | √ | √ | ||
| removed | array with items that where removed | ||||
| addCount | amount of items that where added | ||||
| update | index | index of the single entry that is being updated | √ | ||
| newValue | the newValue that is / will be assigned | √ | √ | ||
| oldValue | the old value that was replaced | ||||
| Map | add | name | the name of the entry that was added | √ | |
| newValue | the new value that is being assigned | √ | √ | ||
| update | name | the name of the entry that is being updated | √ | ||
| newValue | the new value that is being assigned | √ | √ | ||
| oldValue | the value that has been replaced | ||||
| delete | name | the name of the entry that is being removed | √ | ||
| oldValue | the value of the entry that was removed | ||||
| Boxed observable | create | newValue | the value that was assigned during creation. Only observable by spy |
||
| update | newValue | the new value being assigned (only available in intercept and spy ) |
√ | √ | |
| oldValue | .. the old value. Only available in spy |
||||
| (observe callback) | newValue | first param of the observe callback |
|||
| (observe callback) | oldValue | second param of the observe callback previous value |
|||
| Computed observable | (observe callback) | newValue | first param of the observe callback |
||
| (observe callback) | oldValue | second param of the observe callback |
reaction | MobX
Usage: reaction(() => data, data => { sideEffect }, fireImmediately = false, delay = 0)
.
A variation on autorun
that gives more fine grained control on which observables will be tracked.
It takes two functions, the first one is tracked and returns data that is used as input for the second one, the side effect.
Unlike autorun
the side effect won't be run directly when created, but only when the data expression returns a new value for the first time.
Any observables that are accessed while executing the side effect will not be tracked.
The side effect can be debounced, just like autorunAsync
.
reaction
returns a disposer function.
If a string is passed as first argument to reaction
, it will be used as debug name.
The functions passed to when
will receive one argument when invoked, the current reaction, which can be used to dispose the when during execution.
It is important to notice that the side effect will only react to data that was accessed in the data expression, which might be less then the data that is actually used in the effect.
Also, the side effect will only be triggered when the data returned by the expression has changed (the asStructure
modifier can be used to enforce deep comparison).
In other words: reaction requires you to produce the things you need in your side effect.
In the following example both reaction1
, reaction2
and autorun1
will react to the addition, removal or replacement of todo's in the todos
array.
But only reaction2
and autorun
will react to the change of a title
in one of the todo items, because title
is used in the data expression of reaction 2, while it isn't in the data expression of reaction 1.
autorun
tracks the complete side effect, hence it will always trigger correctly, but is also more suspectible to accidentally accessing unrelevant data.
See also what will MobX React to?.
const todos = observable([
{
title: "Make coffee",
done: true,
},
{
title: "Find biscuit",
done: false
}
]);
const reaction1 = reaction(
() => todos.length,
todos => console.log("reaction 1:", todos.map(todo => todo.title).join(", "))
);
const reaction2 = reaction(
() => todos.map(todo => todo.title),
titles => console.log("reaction 2:", titles.join(", "))
);
const autorun1 = autorun(
() => console.log("autorun 1:", todos.map(todo => todo.title).join(", "))
);
todos.push({ title: "explain reactions", done: false });
todos[0].title = "Make tea"
Reaction is roughly speaking sugar for: computed(expression).observe(action(sideEffect))
or autorun(() => action(sideEffect)(expression)
spy | MobX
Usage: spy(listener)
.
Registers a global spy listener that listens to all events that happen in MobX.
It is similar to attaching an observe
listener to all observables at once, but also notifies about running (trans/re)actions and computations.
Used for example by the mobx-react-devtools
.
Spy listeners always receive one object, which usually has at least a type
field. The following events are emitted by default by spy.
| event | fields | nested |
|---|---|---|
| action | name, target (scope), arguments, fn (source function of the action | yes |
| transaction | name, target (scope) | yes |
| scheduled-reaction | object (Reaction instance) | no |
| reaction | object (Reaction instance), fn (source of the reaction) | yes |
| compute | object (ComputedValue instance), target (scope), fn (source) | no |
| error | message | no |
| update (array) | object (the array), index, newValue, oldValue | yes |
| update (map) | object (observable map instance), name, newValue, oldValue | yes |
| update (object) | object (instance), name, newValue, oldValue | yes |
| splice (array) | object (the array), index, added, removed, addedCount, removedCount | yes |
| add (map) | object, name, newValue | yes |
| add (object) | object, name, newValue | yes |
| delete (map) | object, name, oldValue | yes |
| create (boxed observable) | object (ObservableValue instance), newValue | yes |
Note that there are events with the signature { spyReportEnd: true, time? }
.
These events might not have a type
field, but they are part of an earlier fired event that had spyReportStart: true
.
This event indicates the end of an event and this way groups of events with sub-events are created.
This event might report the total execution time as well.
The spy events for observable values are identical to the events passed to observe
. See intercept & observe for an extensive overview.
It is possible to emit your own spy events as well. See extras.spyReport
, extras.spyReportStart
and extras.spyReportEnd
toJS | MobX
toJS(value, supportCycles = true)
Recursively converts an (observable) object to a javascript structure. Supports observable arrays, objects, maps and primitives. Computed values and other non-enumerable properties won't be part of the result. Cycles are detected and properly supported by default, but this can be disabled to improve performance.
For more complex (de)serialization scenario's, one can use serializr
var obj = mobx.observable({
x: 1
});
var clone = mobx.toJS(obj);
console.log(mobx.isObservableObject(obj)); // true
console.log(mobx.isObservableObject(clone)); // false
Note: this method was named toJSON
before MobX 2.2
transaction
transaction(worker: () => void)
can be used to batch a bunch of updates without notifying any observers until the end of the transaction.
transaction
takes a single, parameterless worker
function as argument and runs it.
No observers are notified until this function has completed.
transaction
returns any value that was returned by the worker
function.
Note that transaction
runs completely synchronously.
Transactions can be nested. Only after completing the outermost transaction
pending reactions will be run.
import {observable, transaction, autorun} from "mobx";
const numbers = observable([]);
autorun(() => console.log(numbers.length, "numbers!"));
transaction(() => {
transaction(() => {
numbers.push(1);
numbers.push(2);
});
numbers.push(3);
});
untracked | MobX
Untracked allows you to run a piece of code without establishing observers.
Like transaction
, untracked
is automatically applied by (@)action
, so usually it makes more sense to use actions then untracked
directly.
Example:
const person = observable({
firstName: "Michel",
lastName: "Weststrate"
});
autorun(() => {
console.log(
person.lastName,
",",
untracked(() => person.firstName)
);
});
person.firstName = "G.K.";
person.lastName = "Chesterton";
when | MobX
when(debugName?, predicate: () => boolean, effect: () => void, scope?)
when
observes & runs the given predicate
until it returns true.
Once that happens, the given effect
is executed and the autorunner is disposed.
The function returns a disposer to cancel the autorunner prematurely.
This function is really useful to dispose or cancel stuff in a reactive way. For example:
class MyResource {
constructor() {
when(
() => !this.isVisible,
() => this.dispose()
);
}
@computed get isVisible() {
}
dispose() {
}
}
In MobX 1.0 this method was called autorunUntil
.
autorunAsync
autorunAsync(action: () => void, minimumDelay?: number, scope?)
Just like autorun
except that the action won't be invoked synchronously but asynchronously after the minimum amount of milliseconds has passed.
The action
will be run and observed.
However, instead of running the action immediately when the values it observes have changed, the minimumDelay
will be awaited before re-execution the action
again.
If observed values are changed multiple times while waiting, the action is still triggered only once, so in a sense it achieves a similar effect than a transaction.
This might be useful for stuff that is expensive and doesn't need to happen synchronously; such as debouncing server communication.
If a string is passed as first argument to autorun
, it will be used as debug name.
autorunAsync
returns a disposer to cancel the autorun.
autorunAsync(() => {
sendProfileToServer(profile.asJson);
}, 300);
Atom & Reaction
Atoms
At some point you might want to have more data structures or other things (like streams) that can be used in reactive computations.
Achieving that is pretty simple by using the Atom
class.
Atoms can be used to signal MobX that some observable data source has been observed or changed.
And MobX will signal the atom whenever it is used or no longer in use.
The following example demonstrates how you can create an observable Clock
, which can be used in reactive functions,
and returns the current date-time.
This clock will only actually tick if it is observed by someone.
The complete API of the Atom
class is demonstrated by this example.
import {Atom, autorun} from "mobx";
class Clock {
atom;
intervalHandler = null;
currentDateTime;
constructor() {
this.atom = new Atom(
"Clock",
() => this.startTicking(),
() => this.stopTicking()
);
}
getTime() {
if (this.atom.reportObserved()) {
return this.currentDateTime;
} else {
return new Date();
}
}
tick() {
this.currentDateTime = new Date();
this.atom.reportChanged();
}
startTicking() {
this.tick();
this.intervalHandler = setInterval(
() => this.tick(),
1000
);
}
stopTicking() {
clearInterval(this.intervalHandler);
this.intervalHandler = null;
}
}
const clock = new Clock();
const disposer = autorun(() => console.log(clock.getTime()));
disposer();
Reactions
Reaction
allows you to create your own 'auto runner'.
Reactions track a function and signal when the function should be executed again because one or more dependencies have changed.
This is how autorun
is defined using Reaction
:
export function autorun(view: Lambda, scope?: any) {
if (scope)
view = view.bind(scope);
const reaction = new Reaction(view.name || "Autorun", function () {
this.track(view);
});
if (isComputingDerivation() || globalState.inTransaction > 0)
globalState.pendingReactions.push(reaction);
else
reaction.runReaction();
return reaction.getDisposer();
}
createTransformer
createTransformer(transformation: (value: A) => B, onCleanup?: (result: B, value?: A) => void): (value: A) => B
createTransformer
turns a function (that should transform one value into another value) into a reactive and memoizing function.
In other words, if the transformation
function computes B given a specific A, the same B will be returned for all other future invocations of the transformation with the same A.
However, if A changes, the transformation will be re-applied so that B is updated accordingly.
And last but not least, if nobody is using the transformation of a specific A anymore, it's entry will be removed from the memoization table.
With createTransformer
it is very easy to transform a complete data graph into another data graph.
Transformation functions can be composed so that you can build a tree using lots of small transformations.
The resulting data graph will never be stale, it will be kept in sync with the source by applying small patches to the result graph.
This makes it very easy to achieve powerful patterns similar to sideways data loading, map-reduce, tracking state history using immutable data structures etc.
The optional onCleanup
function can be used to get a notification when a transformation of an object is no longer needed.
This can be used to dispose resources attached to the result object if needed.
Always use transformations inside a reaction like @observer
or autorun
.
Transformations will, like any other computed value, fall back to lazy evaluation if not observed by something, which sort of defeats their purpose.
This all might still be a bit vague, so here are two examples that explain this whole idea of transforming one data structure into another by using small, reactive functions:
Tracking mutable state using immutable, shared data structures.
This example is taken from the Reactive2015 conference demo:
const store = observable({
boxes: [],
arrows: [],
selection: null
});
const states = [];
autorun(() => {
states.push(serializeState(store));
});
const serializeState = createTransformer(store => ({
boxes: store.boxes.map(serializeBox),
arrows: store.arrows.map(serializeArrow),
selection: store.selection ? store.selection.id : null
}));
const serializeBox = createTransformer(box => ({...box}));
const serializeArrow = createTransformer(arrow => ({
id: arrow.id,
to: arrow.to.id,
from: arrow.from.id
}));
In this example the state is serialized by composing three different transformation functions.
The autorunner triggers the serialization of the store
object, which in turn serializes all boxes and arrows.
Let's take closer look at the life of an imaginary example box#3.
- The first time box#3 is passed by
map
toserializeBox
, the serializeBox transformation is executed and an entry containing box#3 and its serialized representation is added to the internal memoization table ofserializeBox
. - Imagine that another box is added to the
store.boxes
list. This would cause theserializeState
function to re-compute, resulting in a complete remapping of all the boxes. However, all the invocations ofserializeBox
will now return their old values from the memoization tables since their transformation functions didn't (need to) run again. - Secondly, if somebody changes a property of box#3 this will cause the application of the
serializeBox
to box#3 to re-compute, just like any other reactive function in MobX. Since the transformation will now produce a new Json object based on box#3, all observers of that specific transformation will be forced to run again as well. That's theserializeState
transformation in this case.serializeState
will now produce a new value in turn and map all the boxes again. But except for box#3, all other boxes will be returned from the memoization table. - Finally, if box#3 is removed from
store.boxes
,serializeState
will compute again. But since it will no longer be using the application ofserializeBox
to box#3, that reactive function will go back to non-reactive mode. This signals the memoization table that the entry can be removed so that it is ready for GC.
So effectively we have achieved state tracking using immutable, shared datas structures here.
All boxes and arrows are mapped and reduced into single state tree.
Each change will result in a new entry in the states
array, but the different entries will share almost all of their box and arrow representations.
Transforming a datagraph into another reactive data graph
Instead of returning plain values from a transformation function, it is also possible to return observable objects. This can be used to transform an observable data graph into a another observable data graph, which can be used to transform... you get the idea.
Here is a small example that encodes a reactive file explorer that will update its representation upon each change. Data graphs that are built this way will in general react a lot faster and will consist of much more straight-forward code, compared to derived data graph that are updated using your own code. See the performance tests for some examples.
Unlike the previous example, the transformFolder
will only run once as long as a folder remains visible;
the DisplayFolder
objects track the associated Folder
objects themselves.
In the following example all mutations to the state
graph will be processed automatically.
Some examples:
- Changing the name of a folder will update it's own
path
property and thepath
property of all its descendants. - Collapsing a folder will remove all descendant
DisplayFolders
from the tree. - Expanding a folder will restore them again.
- Setting a search filter will remove all nodes that do not match the filter, unless they have a descendant that matches the filter.
- Etc.
function Folder(parent, name) {
this.parent = parent;
m.extendObservable(this, {
name: name,
children: m.asFlat([]),
});
}
function DisplayFolder(folder, state) {
this.state = state;
this.folder = folder;
m.extendObservable(this, {
collapsed: false,
name: function() {
return this.folder.name;
},
isVisible: function() {
return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible);
},
children: function() {
if (this.collapsed)
return [];
return this.folder.children.map(transformFolder).filter(function(child) {
return child.isVisible;
})
},
path: function() {
return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name;
}
});
}
var state = m.observable({
root: new Folder(null, "root"),
filter: null,
displayRoot: null
});
var transformFolder = m.createTransformer(function (folder) {
return new DisplayFolder(folder, state);
});
m.autorun(function() {
state.displayRoot = transformFolder(state.root);
});
expr | MobX
expr
can be used to create temporarily computed values inside computed values.
Nesting computed values is useful to create cheap computations to prevent expensive computations to run.
In the following example the expression prevents that the TodoView
component is re-rendered if the selection changes elsewhere.
Instead the component will only re-render when the relevant todo is (de)selected.
const TodoView = observer(({todo, editorState}) => {
const isSelected = mobx.expr(() => editorState.selection === todo);
return
{todo.title}
;
});
expr(func)
is an alias for computed(func).get()
.
extendObservable
Quite similar to Object.assign
, extendObservable
takes two or more arguments, a target
object and one or more properties
maps.
It adds all key-value pairs from the properties to the target
as observable properties.
If an argumentless function is passed as value of a property, extendObservable
will introduce a computed
property instead of an observable property.
var Person = function(firstName, lastName) {
extendObservable(this, {
firstName: firstName,
lastName: lastName
});
}
var matthew = new Person("Matthew", "Henry");
extendObservable(matthew, {
age: 353
});
(N.b: observable(object)
is actually an alias for extendObservable(object, object)
)
isObservable
isObservable
Returns true if the given value was made observable by MobX. Optionally accepts a second string parameter to see whether a specific property is observable.
var person = observable({
firstName: "Sherlock",
lastName: "Holmes"
});
person.age = 3;
isObservable(person);
isObservable(person, "firstName");
isObservable(person.firstName);
isObservable(person, "age");
isObservableMap
Returns true if the given object is created using mobx.map
.
isObservableArray
Returns true if the given object is an array that was made observable using mobx.observable(array)
.
isObservableObject
Returns true if the given object is an object that was made observable using mobx.observable(object)
.
modifiers | MobX
By default, observable
recursively makes all the values of plain objects and arrays recursively observable.
Besides that, it automatically converts functions without arguments into reactive views or derived properties.
For all other types of values just a reference is stored.
In general, this should just do what you need, but if you want you can override the default behavior using modifiers.
Note that modifiers are 'sticky', they are interpreted as being annotations.
They do not only apply to the current value, but also to all values that are assigned in the future to the same attribute.
Note that the attributes class instances (all objects that have a prototype) will not be made observable automatically by observable
.
It is considered to be the responsibility of the class definition / constructor function to mark the necessary attributes of a class instance observable / computed.
asMap
Creates a new Observable Map instead of an Observable Object. See asMap
asReference
The most common modifier is asReference
.
If this modifier is used, observable
will not attempt to make the value observable.
Use this for example if you want to store a reference to a function, instead of creating a view based on that function.
You can also use it to prevent that plain objects or arrays are made observable automatically.
var test = observable({
x : 3,
doubler: function() {
return this.x*2;
},
someFunc: asReference(function() {
return this.x;
})
});
console.log(test.doubler);
console.log(test.someFunc);
asStructure
Can be used on non-cyclic, plain JavaScript values.
Instead of comparing old values with new values based on whether the reference has changed, values are compared using deep equality before notifying any observers.
This is useful if you are working with 'struct' like objects like colors or coordinates and each time return fresh objects with possibly the same values.
asStructure
can be used on reactive functions, plain objects and arrays.
var ViewPort = mobxReact.observer(React.createClass({
displayName: 'ViewPort',
componentWillMount: function() {
mobx.extendObservable({
screenSize: {
width: 0,
height: 0
},
minSize: {
width: 400,
height: 300
},
viewPortSize: mobx.asStructure(function() {
return {
width: Math.max(screenSize.width, minSize.width),
height: Math.max(screenSize.height, minSize.height)
}
}
});
window.onresize = function() {
mobx.transaction(function() {
this.screenSize.width = window.clientWidth;
this.screenSize.height = window.clientHeight;
});
}.bind(this);
},
render: function() {
return (

test

);
}
}));
In the above example, the computed method viewPortSize
returns a fresh object on each re-computation.
So MobX considers it to have changed always. This means that each resize
event of the browser will trigger a re-render of the
ViewPort
component.
However, if the window size is smaller that the minSize
, the resize doesn't need to influence the rendering anymore, as the computed
will return the same dimensions after each run. asStructure
signals to MobX that observers of this computation should only be triggered
if the value returned by the computed has structurally changed (by default strict equality is used to determine whether observers need to be notified).
This means that a new object that is returned from viewPortSize
won't trigger a render
if its contents are (structurally) the same as the previous value.
To use the asStructure
modifier in combination with the @computed
decorator, use the following:
@computed({ asStructure: true }) get viewPortSize() {
return {
width: Math.max(screenSize.width, minSize.width),
height: Math.max(screenSize.height, minSize.height)
}
}
asFlat
Similar to asReference
, except that asFlat
does not prevent its value from becoming observable, but only the children of the value.
It can be used for example to create an observable array or object that should not automatically make its children observable.
var todos = observable(asFlat([{
title: "make coffee",
completed: false
}]));
isObservable(todos);
isObservable(todos[0]);
isObservable(todos[0], "title");
observe | MobX
observe
and intercept
can be used to monitor the changes of a single observable (they don't track nested observables).
intercept
can be used to detect and modify mutations before they are applied to the observable.
observe
allows you to intercept changes after they have been made.
Intercept
Usage: intercept(target, propertyName?, interceptor)
target
: the observable to guardpropertyName
: optional parameter to specify a specific property to intercept. Note thatintercept(user.name, interceptor)
is fundamentally different fromintercept(user, "name", interceptor)
. The first tries to add an interceptor to the currentvalue
insideuser.name
(which might not be an observable at all), the latter intercepts changes to thename
property ofuser
.interceptor
: callback that will be invoked for each change that is made to the observable. Receives a single change object describing the mutation.
The intercept
should tell MobX what needs to happen with the current change.
Therefore it should do one of the following things:
- Return the received
change
object as-is from the function, in wich case the mutation will be applied. - Modify the
change
object and return it, for example to normalize the data. Not all fields are modifiable, see below. - Return
null
, this indicates that the change can be ignored and shouldn't be applied. This is a powerful concept to make your objects for example temporarily immutable. - Throw an exception, for example if some invariant isn't met.
The function returns a disposer
function that can be used to cancel the interceptor.
It is possible to register multiple interceptors to the same observable.
They will be chained in registration order.
If one of the interceptors returns null
or throw an exception, the other interceptors won't be evaluated anymore.
It is also possible to register an interceptor both on a parent object and on an individual property.
In that case the parent object interceptors are run before the property interceptors.
const theme = observable({
backgroundColor: "#ffffff"
})
intercept(theme, "backgroundColor", change => {
if (!change.newValue) {
return null;
}
if (change.newValue.length === 6) {
change.newValue = '#' + change.newValue;
return change;
}
if (change.newValue.length === 7) {
return change;
}
throw new Error("This doesn't like a color at all: " + change.newValue);
})
Observe
Usage: observe(target, propertyName?, listener, invokeImmediately?)
target
: the observable to observepropertyName
: optional parameter to specify a specific property to observe. Note thatobserve(user.name, listener)
is fundamentally different fromobserve(user, "name", listener)
. The first observes the currentvalue
insideuser.name
(which might not be an observable at all), the latter observes thename
property ofuser
.listener
: callback that will be invoked for each change that is made to the observable. Receives a single change object describing the mutation, except for boxed observables, which will invoke thelistener
two parameters:newValue, oldValue
.invokeImmediately
: by default false. Set it to true if you wantobserve
to invokelistener
directly with the state of the observable (instead of waiting for the first change). Not supported (yet) by all kinds of observables.
The function returns a disposer
function that can be used to cancel the observer.
Note that transaction
does not affect the working of the observe
method(s).
This means that even inside a transaction observe
will fire its listeners for each mutation.
Hence autorun
is usually a more powerful and declarative alternative to observe
.
Example:
import {observable, observe} from 'mobx';
const person = observable({
firstName: "Maarten",
lastName: "Luther"
});
const disposer = observe(person, (change) => {
console.log(change.type, change.name, "from", change.oldValue, "to", change.object[change.name]);
});
person.firstName = "Martin";
disposer();
const disposer2 = observe(person, "lastName", (newValue, oldValue) => {
console.log("LastName changed to ", newValue);
});
Related blog: Object.observe is dead. Long live mobx.observe
Event overview
The callbacks of intercept
and observe
will receive an event object which has at least the following properties:
object
: the observable triggering the eventtype
: (string) the type of the current event
These are the additional fields that are available per type:
| observable type | event type | property | description | available during intercept | can be modified by intercept |
|---|---|---|---|---|---|
| Object | add | name | name of the property being added | √ | |
| newValue | the new value being assigned | √ | √ | ||
| update | name | name of the property being updated | √ | ||
| newValue | the new value being assigned | √ | √ | ||
| oldValue | the value that is replaced | ||||
| Array | splice | index | starting index of the splice. Splices are also fired by push , unshift , replace etc. |
√ | |
| removedCount | amount of items being removed | √ | √ | ||
| added | array with items being added | √ | √ | ||
| removed | array with items that where removed | ||||
| addCount | amount of items that where added | ||||
| update | index | index of the single entry that is being updated | √ | ||
| newValue | the newValue that is / will be assigned | √ | √ | ||
| oldValue | the old value that was replaced | ||||
| Map | add | name | the name of the entry that was added | √ | |
| newValue | the new value that is being assigned | √ | √ | ||
| update | name | the name of the entry that is being updated | √ | ||
| newValue | the new value that is being assigned | √ | √ | ||
| oldValue | the value that has been replaced | ||||
| delete | name | the name of the entry that is being removed | √ | ||
| oldValue | the value of the entry that was removed | ||||
| Boxed observable | create | newValue | the value that was assigned during creation. Only observable by spy |
||
| update | newValue | the new value being assigned (only available in intercept and spy ) |
√ | √ | |
| oldValue | .. the old value. Only available in spy |
||||
| (observe callback) | newValue | first param of the observe callback |
|||
| (observe callback) | oldValue | second param of the observe callback previous value |
|||
| Computed observable | (observe callback) | newValue | first param of the observe callback |
||
| (observe callback) | oldValue | second param of the observe callback |
reaction | MobX
Usage: reaction(() => data, data => { sideEffect }, fireImmediately = false, delay = 0)
.
A variation on autorun
that gives more fine grained control on which observables will be tracked.
It takes two functions, the first one is tracked and returns data that is used as input for the second one, the side effect.
Unlike autorun
the side effect won't be run directly when created, but only when the data expression returns a new value for the first time.
Any observables that are accessed while executing the side effect will not be tracked.
The side effect can be debounced, just like autorunAsync
.
reaction
returns a disposer function.
If a string is passed as first argument to reaction
, it will be used as debug name.
The functions passed to when
will receive one argument when invoked, the current reaction, which can be used to dispose the when during execution.
It is important to notice that the side effect will only react to data that was accessed in the data expression, which might be less then the data that is actually used in the effect.
Also, the side effect will only be triggered when the data returned by the expression has changed (the asStructure
modifier can be used to enforce deep comparison).
In other words: reaction requires you to produce the things you need in your side effect.
In the following example both reaction1
, reaction2
and autorun1
will react to the addition, removal or replacement of todo's in the todos
array.
But only reaction2
and autorun
will react to the change of a title
in one of the todo items, because title
is used in the data expression of reaction 2, while it isn't in the data expression of reaction 1.
autorun
tracks the complete side effect, hence it will always trigger correctly, but is also more suspectible to accidentally accessing unrelevant data.
See also what will MobX React to?.
const todos = observable([
{
title: "Make coffee",
done: true,
},
{
title: "Find biscuit",
done: false
}
]);
const reaction1 = reaction(
() => todos.length,
todos => console.log("reaction 1:", todos.map(todo => todo.title).join(", "))
);
const reaction2 = reaction(
() => todos.map(todo => todo.title),
titles => console.log("reaction 2:", titles.join(", "))
);
const autorun1 = autorun(
() => console.log("autorun 1:", todos.map(todo => todo.title).join(", "))
);
todos.push({ title: "explain reactions", done: false });
todos[0].title = "Make tea"
Reaction is roughly speaking sugar for: computed(expression).observe(action(sideEffect))
or autorun(() => action(sideEffect)(expression)
spy | MobX
Usage: spy(listener)
.
Registers a global spy listener that listens to all events that happen in MobX.
It is similar to attaching an observe
listener to all observables at once, but also notifies about running (trans/re)actions and computations.
Used for example by the mobx-react-devtools
.
Spy listeners always receive one object, which usually has at least a type
field. The following events are emitted by default by spy.
| event | fields | nested |
|---|---|---|
| action | name, target (scope), arguments, fn (source function of the action | yes |
| transaction | name, target (scope) | yes |
| scheduled-reaction | object (Reaction instance) | no |
| reaction | object (Reaction instance), fn (source of the reaction) | yes |
| compute | object (ComputedValue instance), target (scope), fn (source) | no |
| error | message | no |
| update (array) | object (the array), index, newValue, oldValue | yes |
| update (map) | object (observable map instance), name, newValue, oldValue | yes |
| update (object) | object (instance), name, newValue, oldValue | yes |
| splice (array) | object (the array), index, added, removed, addedCount, removedCount | yes |
| add (map) | object, name, newValue | yes |
| add (object) | object, name, newValue | yes |
| delete (map) | object, name, oldValue | yes |
| create (boxed observable) | object (ObservableValue instance), newValue | yes |
Note that there are events with the signature { spyReportEnd: true, time? }
.
These events might not have a type
field, but they are part of an earlier fired event that had spyReportStart: true
.
This event indicates the end of an event and this way groups of events with sub-events are created.
This event might report the total execution time as well.
The spy events for observable values are identical to the events passed to observe
. See intercept & observe for an extensive overview.
It is possible to emit your own spy events as well. See extras.spyReport
, extras.spyReportStart
and extras.spyReportEnd
toJS | MobX
toJS(value, supportCycles = true)
Recursively converts an (observable) object to a javascript structure. Supports observable arrays, objects, maps and primitives. Computed values and other non-enumerable properties won't be part of the result. Cycles are detected and properly supported by default, but this can be disabled to improve performance.
For more complex (de)serialization scenario's, one can use serializr
var obj = mobx.observable({
x: 1
});
var clone = mobx.toJS(obj);
console.log(mobx.isObservableObject(obj)); // true
console.log(mobx.isObservableObject(clone)); // false
Note: this method was named toJSON
before MobX 2.2
transaction
transaction(worker: () => void)
can be used to batch a bunch of updates without notifying any observers until the end of the transaction.
transaction
takes a single, parameterless worker
function as argument and runs it.
No observers are notified until this function has completed.
transaction
returns any value that was returned by the worker
function.
Note that transaction
runs completely synchronously.
Transactions can be nested. Only after completing the outermost transaction
pending reactions will be run.
import {observable, transaction, autorun} from "mobx";
const numbers = observable([]);
autorun(() => console.log(numbers.length, "numbers!"));
transaction(() => {
transaction(() => {
numbers.push(1);
numbers.push(2);
});
numbers.push(3);
});
untracked | MobX
Untracked allows you to run a piece of code without establishing observers.
Like transaction
, untracked
is automatically applied by (@)action
, so usually it makes more sense to use actions then untracked
directly.
Example:
const person = observable({
firstName: "Michel",
lastName: "Weststrate"
});
autorun(() => {
console.log(
person.lastName,
",",
untracked(() => person.firstName)
);
});
person.firstName = "G.K.";
person.lastName = "Chesterton";
when | MobX
when(debugName?, predicate: () => boolean, effect: () => void, scope?)
when
observes & runs the given predicate
until it returns true.
Once that happens, the given effect
is executed and the autorunner is disposed.
The function returns a disposer to cancel the autorunner prematurely.
This function is really useful to dispose or cancel stuff in a reactive way. For example:
class MyResource {
constructor() {
when(
() => !this.isVisible,
() => this.dispose()
);
}
@computed get isVisible() {
}
dispose() {
}
}
In MobX 1.0 this method was called autorunUntil
.