Redux vs. MobX by example — Part II: The Simplicity of MobX, and Conclusion
Continued from Redux vs. MobX by example — Part I: Intro, and Exploring Redux…
While Redux has an opinionated structure for managing your state; with MobX, you’re free to architect the structure and location of state in your app, as you see it fit.
MobX is very simple and straightforward. If you are aware of the implementation details of our original todo app; it is not hard to comprehend the working principles of MobX, just by grokking the few specific changes needed for implementing MobX in our todo app.
Without further ado, let’s jump straight to implementation. We will also delve into the base MobX concepts ahead. Following is an overview of what we will cover in this story.
- Setup for MobX
- The Gist of MobX
@observable
— Making your state observable@observer
— A view that responds to changes in state@computed
— Anything that can be derived from the state, should be@observer
— Optimising therender()
of React components
- The Redux vs. MobX Conclusion
Setup for MobX
If you’re continuing from Part I of this story; from your project’s root folder…
$ git checkout master
$ git checkout -b mobx-implementation
$ npm install --save mobx mobx-react babel-plugin-transform-decorators-legacy
If you’re here for the first time
$ git clone https://github.com/fatman-/minimal-todo-react.git
$ cd minimal-todo-react
$ npm install
$ git checkout -b mobx-implementation
$ npm install --save mobx mobx-react babel-plugin-transform-decorators-legacy
We need the transform-decorators-legacy
plugin to enable support for ES7 @decorators
.
The Gist of MobX
State, Derivations, and Actions, constitute the core concepts of MobX.
State: State is the data that drives your application
Derivations: Anything that can be derived from the state, without any further interaction, is a derivation. There are two kinds of derivations in MobX:
- Computed values — These are values that can always be derived from the current observable state using a pure function.
- Reactions — Reactions are side effects that need to happen automatically if the state changes. Rendering of a React component, because of a change in some observable data, would be an example of a Reaction.
Actions: An action is any piece of code that changes the state.
The fact that MobX is so elegantly simple, is reflected through the following principles of MobX. Creating a MobX app boils down to the following steps.
- Define your state and make it observable
- Create a view that [observes and] responds to the changes in the State
- Modify the state … magic!
Let’s see how the above concepts & principles of MobX transform to code.
@observable
— Making your state observable
The todos
array in the TodoInterface.js
library; is the state of the app. To make it observable, all we have to do is add an @observable
decorator to it.
./src/lib/TodoDataInterface.js
import { observable, action } from 'mobx';
import Todo from './Todo';
import { findIndex } from 'lodash';
export default class TodoDataInterface {
// This is just syntactic sugar for: const todos = observable([])
@observable todos = [];
...
This is the only needed change here. Apart from this, we will also add @action
decorators to all the methods manipulating the todos
array.
While we’re not using MobX’s strict mode — which makes changing state impossible unless you’re doing it using an “action” — by explicitly marking the methods that change state, as actions, we can structure and reason about our code in a better way.
./src/lib/TodoDataInterface.js
...
constructor() {
this.completeTodo = this.completeTodo.bind(this);
this.removeTodo = this.removeTodo.bind(this);
}
@action
addTodo(descriptionText) {
if (descriptionText) {
const newTodo = new Todo(descriptionText);
this.todos.push(newTodo);
return newTodo;
}
}
@action
completeTodo(todoId) {
const todoIndex = findIndex(this.todos, (todo) => todo.id === todoId);
if (todoIndex > -1) {
this.todos[todoIndex].isDone = !this.todos[todoIndex].isDone
}
}
@action
removeTodo(todoId) {
const todoIndex = findIndex(this.todos, (todo) => todo.id === todoId);
if (todoIndex > -1) {
this.todos.splice(todoIndex, 1);
}
}
}
@observer
— A view that responds to changes in state
mobx-react
provides an observer
function, which we can use to decorate our React components, to make them reactive to changes in all observables present in our app.
Decorating a React component with the @observer
decorator will wrap the component’s render()
function with MobX’s autorun()
function, which will make sure of a re-render, if any observable data is changed (in our case, the todos
array inside the TodoInterface.js
file).
./src/components/TodoApp.js
import React from 'react';
import { observable, computed } from 'mobx';
import { observer } from 'mobx-react';
import VisibleTodoList from './VisibleTodoList';
@observer
export default class TodoApp extends React.Component {
@observable visibilityFilter = "ACTIVE_TODOS"
visibilityFilters = ["ALL_TODOS", "ACTIVE_TODOS", "COMPLETED_TODOS"];
changeVisibilityFilter = visibilityFilter => {
this.visibilityFilter = visibilityFilter;
}
@computed get visibleTodos() {
switch (this.visibilityFilter) {
case "ALL_TODOS":
return this.props.dataInterface.todos;
case "ACTIVE_TODOS":
return this.props.dataInterface.todos.filter(todo => todo.isDone === false);
case "COMPLETED_TODOS":
return this.props.dataInterface.todos.filter(todo => todo.isDone === true);
default:
return this.props.dataInterface.todos;
}
}
render() {
return (
<div>
<h2> Minimal TodoApp built with React and MobX </h2>
<input
type="text"
placeholder="What do you want todo?"
ref={(c => this._todoInputField = c)}
/>
<button
onClick={() => {
this.props.dataInterface.addTodo(this._todoInputField.value);
this._todoInputField.value = "";
}}>
Add Todo
</button>
<VisibleTodoList
visibleTodos={this.visibleTodos}
visibilityFilter = {this.visibilityFilter}
completeTodo={this.props.dataInterface.completeTodo}
removeTodo={this.props.dataInterface.removeTodo}
/>
<div>
SHOW:
{
this.visibilityFilters.map(
visibilityFilter =>
<button
key={visibilityFilter}
onClick={() => this.changeVisibilityFilter(visibilityFilter)}>
{visibilityFilter.replace("_", " ")}
</button>
)
}
</div>
</div>
);
}
}
@computed
— Anything that can be derived from the state, should be
If you notice the above code for the TodoApp
component, we are using an @observable
decorator for the visibiltyFilter
.
This is a good example of how component level state can also be managed by making the required variables into observables.
Notice the @computed
decorator? this.visibleTodos
(which is being passed as a prop to the VisibleTodoList
component) is a derivation which happens whenever there is a change in the visibilityFilter
observable.
@observer
— Optimising the render()
of React components
@observer
only works on the top-level component, it is applied upon; not to the child components rendered by it. Any component that renders observable data should be decorated with @observer
.
@mweststrate has explained this in a crystal clear way, in one of his comments:
In general I recommend to use
@observer
s everywhere though, simply because it will be better optimized. If children components have their own@observer
, it means that the parent doesn't need to be re-rendered when the child data changes. That means that other children don't have to be reconciled, which is a pretty expensive process in React when having large collections
We will go ahead and add @observer
decorators to the child components VisibleTodoList
and SingleTodo
…
./src/components/VisibleTodoList.js
import React from 'react';
import { observer } from 'mobx-react';
@observer
export default class VisibleTodoList extends React.Component {
...
./src/components/SingleTodo.js
import React from 'react';
import { observer } from 'mobx-react';
@observer
export default class SingleTodo extends React.Component {
...
…and with that, we’re done. Start the app to check everything is in working condition.
From your project’s root folder
$ npm start
👏🏼
The Redux vs. MobX conclusion
Having completed both the Redux and MobX implementations, here are a few notable differences, that can be garnered from this simple example.
The differences really boil down to MobX being automatic, having a lot of magic happening behind the scenes; and Redux being manual and explicit.
Redux is essentially a store container, you have a single immutable store object, with APIs to dispatch actions to update the store, and to subscribe to changes in the state; with set guidelines for structuring and manipulating your state.
MobX keeps things simple and straightforward, by passing on the architectural control to your hands. There is no state container. You are free to structure state as you see fit.
The Redux requirement of being explicit about all the actions, reducers, the whole Redux pipeline, brings in a lot of predictability, but also introduces a lot of boilerplate code.
With observable state, and observer components; MobX implementation has considerably less amount of code; not only when compared with the Redux implementation, but also the pure React implementation. No more
setState()
calls. 😃
We use Redux quite a lot at Hashnode, it is great! Having said that, it is hard to not talk about how life becomes beautiful with MobX. Haha! 😄
As someone has quite awesomely put it in a Hacker News comment, picked from here:
“MobX, it's been mentioned elsewhere but I can't help but sing its praises. Writing in MobX means that using controllers/ dispatchers/ actions/ supervisors or another form of managing dataflow returns to being an architectural concern you can pattern to your application's needs, rather than being something that's required by default for anything more than a Todo app.
Hope this comparison is of help to someone. ✌🏻 Let me know if you have any “Redux vs. MobX” opinions in the comments. 🙂
P.S.: You shouldn't miss this answer by @lorefnon ... an incredible high-level overview of the Redux vs. MobX differences.