ReactT2 Dark_1200x303

Learn about a fantastic state management library, MobX, and how to use it in your React projects.

State management is an important topic in every application nowadays. When we’re starting to define the bare bones of our application, after we define which framework or library we will use, the next topic to define is state management.

We have to decide: Should we use a state management library? Which one should we use?

We know for a fact that now, in React, we have a lot of solutions for state management. We can use React Hooks, a functionality released in the 16.7 version that allows us to manage and deal with our state data in functional components, which means that we can stop using class components just for state management. Or, we can use the library that’s been the most widely used in the React community for a long time, Redux.

But there’s another library, one not as famous as Redux, that can help us manage our state data, and it might be worth your consideration.

MobX

MobX is a simple, scalable and powerful state management library. Much like React, which uses a virtual DOM to render UI elements in our browsers, reducing the number of DOM mutations, MobX does the same thing but in our application state.

MobX was created using TFRP (transparently applying functional reactive programming). We can understand it as being a reactive library: It makes our application state consistent and bug-free by using a reactive dependency state graph that will be only updated when needed.

A statement that defines MobX very well is:

Anything that can be derived from the application state, should be derived. Automatically.

MobX has some big differences from Redux. For example, in Redux we can only have one single store in which we store all of our state data; in MobX we can have multiple stores for different purposes. Another big difference is that, in Redux, our state is immutable, while in MobX we can mutate our state.

A lot of people like to explain that MobX treats your application state like a spreadsheet, which is true. Another nice thing about MobX is that it’s a framework-agnostic library, which means you can use it in other JS environments as well, or even in other languages—for example, Flutter.

MobX has four principle concepts that we should learn, to understand how it works: observables, computed values, reactions and actions.

Four boxes: Observable, Computed, Reaction, Action. Observable has two sets of arrows: "Updates" to Computed, and "Triggers" to Reaction. Computed has an arrow "Triggers" to Reaction. Reaction has an arrow "Events" to Action. Action has an arrow "Modify" to Observable.

Observables

Observables in MobX allow us to add observable capabilities to our data structures—like classes, objects, arrays—and make our properties observables. That means that our property stores a single value, and whenever the value of that property changes, MobX will keep track of the value of the property for us.

For example, let’s imagine we have a variable called counter. We can easily create an observable by using the @observable decorator, like this:

import { observable } from "mobx";
class CounterStore {
  @observable counter = 0
}

By using the @observable decorator, we’re telling MobX that we want to keep track of the value of counter, and every time the counter changes, we’ll get the updated value.

If you don’t want to use the @observable decorator, you can use decorate to make your object observable:

import { decorate, observable } from "mobx";
class CounterStore {
  counter = []
}

decorate(CounterStore, {
  counter: observable
})

Computed Values

In MobX, we can understand computed values as values that can be derived from our state, so the name “computed values” makes total sense. They’re basically functions that are derived from our state, so whenever our state changes, their return values will change as well.

One thing that you must remember about computed values is that the get syntax is required, and the derivation that it makes from your state is automatic—you don’t need to do anything to get the updated value.

To have computed values, you can use the @computed decorator, like this:

import { observable } from "mobx";
class CounterStore {
  @observable counter = 0
  @computed get counterMoreThan100() {
    return this.counter > 100
  }
}

Or, if you don’t want to use the @computed decorator, you can use decorate too:

import { observable, computed } from "mobx";
class CounterStore {
  counter = 0
    get counterMoreThan100() {
      return this.counter > 100
    }
}

decorate(CounterStore, {
  counter: observable,
  counterMoreThan100: computed
})

Actions

Actions in MobX are a very important concept because they’re responsible for modifying our state. They’re responsible for changing and modifying our observables.

To create actions in MobX, you can use the @action decorator, like this:

import { observable } from "mobx";
class CounterStore {
  @observable counter = 0
  @computed get counterMoreThan100() {
    return this.counter > 100
  }
  @action incrementCounter() {
    return this.counter + 1
  }
}

Reactions

Reactions in MobX are pretty similar to computed values, but the difference is that reactions trigger side effects and occur when our observables change. Reactions in MobX can either be UI changes or background changes—for example, a network request, a print on the console, etc.

We have the custom reactions: autorun, reaction, when.

Autorun

Autorun will run every time a specific observable changes. For example, if we wanted to print the value of counter every time it changes, we could do like this:

autorun(() => {
  console.log(`Counter changed to: ${this.counter}`)
})

The autorun reaction receives a second argument; this second argument can be an object with the following options:

  • delay: You can debounce your autorun reaction by passing a number; if you don’t pass anything, no debounce will happen.
  • name: You can name your autorun reaction by passing a string.
  • onError: You can pass a function to handle the errors of your autorun reaction.
  • scheduler: You can schedule and determine if you want to re-run your autorun function by scheduling it. It takes a function that should be invoked at some point in the future, for example: { scheduler: run => { setTimeout(run, 1000) }}.

Reaction

Reaction is very similar to autorun, but it gives you more control over which observables’ values should be tracked. It receives two arguments: the first one is a simple function to return the data to be used in the second argument. The second argument will be the effect function; this effect function will only react to data that was passed in the first function argument. This effect function will only be triggered when the data that you passed in the first argument has changed.

reaction(() => data, (data, reaction) => { sideEffect }, options?)

For example, let’s create a reaction to print in the console when the counter it’s greater than 100.

const customReactionCounter = reaction(
  () => counter.length > 100,
  counter => console.log(`Counter is: ${counter}`)
)

The reaction also receives a third argument, which can be an object with the following options:

  • fireImmediately: It’s a boolean value and it indicates if the function should be triggered after the first run of the data function.
  • delay: You can debounce your reaction function by passing a number; if you don’t pass anything, no debounce will happen.
  • equals: comparer.default by default. If specified, this comparer function will be used to compare the previous and next values produced by the data function. The effect function will only be invoked if this function returns false. If specified, this will override compareStructural.
  • name: You can name your autorun reaction by passing a string.
  • onError: You can pass a function to handle the errors of your autorun reaction.
  • scheduler: You can schedule and determine if you want to re-run your function by scheduling it.

The when Reaction

Of the three custom reactions in MobX, when is my favorite by far because it’s very easy to understand when you’re learning and seems to give you superpowers in your code.

The when reaction is a function that receives two arguments: first is a predicate, which will run when the returned value is true, and the second argument is an effect.

when(predicate: () => boolean, effect?: () => void, options?)

So, for example, let’s imagine that we have a when reaction in our code, and every time the value of counter is greater than 100, we want to increment our counter. This is how we can do that with the when reaction:

class CounterStore {
  constructor() {
    when(
      () => this.counterMoreThan100,
      () => this.incrementCounter
    )
  }
  @observable counter = 0
  @computed get counterMoreThan100() {
    return this.counter > 100
  }
  @action incrementCounter() {
    return this.counter + 1
  }
}

Now that we learned about the four principle concepts of MobX, let’s build something so we can apply them and learn how it all works in practice.

Getting Started with MobX

We’re going to create a simple application that we can add and remove books. So let’s just create a new create-react-app.

npx create-react-app mobx-example

Let’s now install the two dependencies that we’re going to need to use MobX: mobx and mobx-react.

yarn add mobx mobx-react

Now, we’re going to create our store. We’re going to have a observable called books, which will be an array of books, and two actions: addBook and removeBook. Don’t forget to add the respective decorators: the @observable decorator for books and the @action decorator for our addBook and removeBook functions.

import { observable, action } from 'mobx';
class BooksStore {
  @observable books = [];
  @action addBook = (book) => {
    this.books.push(book)
  }
  @action removeBook = (index) => {
    this.books.splice(index, 1)
  }
}

export default BooksStore

Now that we’ve created our store, we need to have access to it. We’re going to use the Provider from mobx-react, so in our index.js file, just import the Provider from mobx-react and wrap around our App component, like this:

import { Provider } from 'mobx-react';
<Provider booksStore={booksStore}>
<App />
</Provider>

This Provider component works pretty much like the Redux provider. We have a store and we can use this Provider to pass down stores using React’s context mechanism.

Now, in our App component, let’s import some things.

import { observer, inject } from 'mobx-react';

To inject or connect our stores to our App component, we’re going to use inject. It’ll make our stores (in our case, the booksStore) available to our component. We’ll use the observer to make our App component reactive, which means that this component will track which observables we’re using in our render and automatically re-render every time our observables (in our case, the books array) changes.

Now, we’re going to wrap our App component with both functions, and it will look like this:

const App = inject(["booksStore"])(observer(({ _booksStore_ }: _any_) => {

...

}));

Let’s create a div, and inside that div we’re going to have an input and a button. We’re going to use the useState hook from React to manage state inside our component, just to keep track of the value of the input.

So, let’s now create our state using useState, and a new function to add a book every time we click the button.

const [newBook, setNewBook] = useState('');
const addNewBook = () => {
  if (!newBook) return;
  booksStore.addBook(newBook);
  setNewBook("");
}

In our input, we’re going to pass the newBook as a value, and an onChange method. In our button, we’re going to add addNewBook to run every time we click the button.

<input _type_="text" _value_={newBook} _onChange_={(_e_) => setNewBook(e.target.value)} />
<button _onClick_={addNewBook}>add</button>

We don’t have any books yet. Let’s now map our books array, and for each book, we’re going to add an onClick to delete a book.

{booksStore.books.map((_book_, _index_) => (
  <h1 _key_={index} _onClick_={() => booksStore.removeBook(index)}>{book}</h1>
))}

Our final component is going to look like this:

const App = inject(["booksStore"])(observer(({ _booksStore_ }: _any_) => {
  const [newBook, setNewBook] = useState<_string_>('');
  const addNewBook = () => {
    if (!newBook) return;
    booksStore.addBook(newBook);
    setNewBook("");
  }

  return (
    <div>
    {booksStore.books.map((_book_: _string_, _index_: _number_) => (
    <h1 _key_={index} _onClick_={() => booksStore.removeBook(index)}> {book}
    </h1>
  ))}
  <input _type_="text" _value_={newBook} _onChange_={(_e_: _any_) => setNewBook(e.target.value)} />
  <button _onClick_={addNewBook}>add</button>
  </div>
  )
}));

We should now have a simple component working that allows us to add and remove books. As you can see, MobX is very simple to start, and we can build powerful things with it. If you’re considering moving to this library, I’d really recommend you to do it.

Conclusion

In this article, we learned how MobX works, including how it’s different from Redux, as well as its four main concepts: observables, computed values, actions, reactions. And then we created an example to test it in practice.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.