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.
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.