The United States of MobX

Antoine Jaussoin
Around the App in 365 days
5 min readMay 4, 2018

Last week, we added some simple routing to our application.

This week, we will talk state management. This is a big one!

What is state management?

In any application, your user will do some actions, fetch some data from the server, modify that data, create some data of his own, toggle some part of the UI, and all of this is called “the state” of the application. And that state needs to be stored somewhere!

So far, we stored that state (the list of cards, the selections, etc.) directly in the various components, using setState. This is good, it works, but will soon lead to problems. For once, if you store a piece of information in a component, you can’t easily access it from another component. If that other component was a child, you’d need to pass that information as a prop, sometimes many level down. If it was a parent, you would need to move the storage of that information higher up until that parent component.

Enter the concept of state management: instead of storing the data using React’s setState, we will outsource that work to a dedicated library.

There are a few choices out there, the main ones being Redux, the new React Context, and MobX.

Choosing between them is almost a religious question, but this article should help a bit.

For our project we will be using MobX, because our state should be relatively simple and MobX is less verbose, has less boilerplate.

Installing MobX

MobX and mobx-react (commit)

In your terminal, add both MobX and its React bindings:

yarn add mobx mobx-react

The issue with decorators (commit)

Now, to use MobX properly, we’ll use decorators. The problem is that a create-react-app application doesn’t support decorators natively, so we’ll need to tweak the webpack config, using a magic library called create-app-rewired.

What this library does is to allow tweaking the (hidden) webpack configuration, without having to eject the app.

First, you need to install these:

yarn add react-app-rewired react-app-rewire-mobx

Then, you’ll need to create a file at the root of your project, called config-overrides.js:

const rewireMobX = require('react-app-rewire-mobx');module.exports = function override(config, env) {
config = rewireMobX(config, env);
return config;
};

You also need to modify your package.json file to use the react-app-rewired scripts instead of the usual react-scripts:

"client": "react-app-rewired start",

Creating the store (commit)

In this commit, we will create our first store, in src/store/index.js.

A store is an object that will hold your state, your data, and thanks to the magic of MobX, will re-render your components when the data changes.

For React to be aware of these changes, you need to decorate any property that is susceptible to change with the @observable decorator.

To change one of these value, the best practice is to use an @action method, that will mutate this value. You could in theory mutate the value yourself directly, but this is bad practice.

Our store, for now, is pretty simple: we have one property (uername), on method to change this property (changeUsername), and a constructor that defaults the username to “Unknown Player”.

Now, for the store to be available to our components, we will need to instantiate it and make it available to our React component tree using the context. You can see this in action here, line 11.

On line 14, we wrap our entire application in a <Provider> that will put our store in the React context to make it available to our other components.

Finally, it’s time to consume the store: on line 43 and 44, we add two decorators:

  • @observer: this one is compulsory on any component that uses a MobX store. It makes the React component subscribe to any change on that store. If you forget to add this decorator, your component might not update when your store changes.
  • @inject(‘store’): this pass the store as a prop, which will be useful to access the store within the component.

On line 107 and 111, you can see how we get the store from the props, and read the username value from the store (instead of using the component’s state). The username is updated on line 98 using the changeUsername method on the store.

Moving the entire state into the store (commit)

In this commit, we are moving the rest of the state (the cards, the selection, the list of players) into the store, as well as handling the socket IO connection.

The game.js file becomes much easier to read, only taking care of UI concerns, which should have been the case all along.

We are not going to go through all the changes line by line, but we will instead highlight the most important aspects of these changes.

Separating the API and Socket IO calls from the store

As you can see on the store constructor, line 14, we pass two objects: a transport object (for managing all the socket IO communication), and an API object (for REST Api endpoints).

This is very important to do this for testing: on our tests, we will be able to mock both classes, and test the rest of the store without having to make actual connections to a server.

We will tackle unit testing in a following article.

Using MobX “enforceActions” (AKA “strict mode”)

In the src/index.js file, you can notice on line 14 that we added a configuration bit for MobX, setting the enforceActions flag to true: what it does is make MobX throw an exception if we mutate any observable in the store outside of an action. This is to enforce the best practice we mentioned earlier in this article.

What the hell is runInAction()

As mentioned in the previous paragraph, any mutation needs to happen within an action. On line 46, although we are supposed to be in the loadCards action, we are using this weird runInAction function… what gives?

Well, if you look closely, you will see that the previous line (45) is asynchronous: that means that the next line is not run in the same (synchronous) function context (it can be run minutes after, depending on the speed of your internet connection etc.), and so you need to wrap any call mutating your state after an asynchronous call with runInAction. More information here if you need.

Handling disconnecting users (commit)

In this commit, we handle the case where a user disconnects from the game, either by returning to the home page, or by closing the web page (or browser) entirely.

This will be caught by the server, which will then update all the other players with the new list of users, removing the leaving user from the list.

--

--