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 the render() 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 @observers 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.

Write your comment…

6 comments

There is a mistake in the new commit that was supposed to fix the problem when clicking the checkboxes.

@observable this.isDone;

should be

@observable isDone;

Thanks for the heads-up @marekp! I'd fixed this locally but forgot to push the change to remote. Here is the fixed commit.

Reply to this…

Share your programming knowledge and learn from the best developers on Hashnode

Get started

Looks like current mobx branch code does not respond to checkbox action. What is the process to figure out what went wrong with MobX magic?

Hi, @alexvalex! Thanks for pointing out the error. It was due to this commit.

From MobX's @observable documentation:

If value is an object with a prototype, a JavaScript primitive or function, a Boxed Observable will be returned. MobX will not make objects with a prototype automatically observable; as that is the responsibility of its constructor function. Use extendObservable in the constructor, or @observable in its class definition instead.

By removing the observable on the isDone property of the Todo objects in the above commit, MobX was not reacting to the changes in Todo objects.

This is now fixed, in the mobx branch; and everything should work as expected. Let me know, if you run into any more problems. :)

Reply to this…

Great tutorial, thanks! However, it is not very clear to me how come that the list of visible todos gets updated when user adds a new todo. Seems like a real magic to me. How does the TodoApp learn about the updated todos?

We have specified, the todos array as an @observable. Whenever we make any changes to an @observable; the components decorated with @observer will autorun/rerun the render method.

Similarly, with @computed, whenever an @observable changes; the functions decorated with @computed are run, which get the required properties from the@observable data, and they compute the derivations, as required... and that's how we get visibleTodos.

If you're interested to know more, check out the MobX docs. They're very well written.

Reply to this…