Converting a small demo app from React useState to MobX-State-Tree

Recently, I did a little Twitter thread converting a small React demo app (in Next.js) to MobX-State-Tree, which we use at my company.

Here’s a blog-form version of that Twitter thread.

React State

The code is using useState . Nothing too special — just a value for width and a value for height and a setter for each.

The results are then rendered normally in the component.

This is a simple app, but to simulate a more complex app, I’m going to break those into separate components.

First, I create a DimensionInput component.

It takes props for name, value, and onChange.

The Result component looks like this:

It takes props for title and value.

All of the state is “hoisted” up to the common parent so that it can be shared between the inputs and the calculation results. For a simple app, this structure works pretty well.

However, you’ll see if you enable the “Highlight updates” feature of React Dev Tools that the whole page of components re-render needlessly when changing the values.

Converting to MobX-State-Tree and MobX-React-Lite

First, I install the three packages, mobx, mobx-state-tree, and mobx-react-lite. (MobX-React-Lite is a smaller version of MobX-React that only supports function components, which is all we need.)

Then, I create a RootStore in a ./store.js file. I left it in the Next.js pages folder, which isn’t ideal, but it’ll work for this demo.

MobX-State-Tree exports a types object that lets me create stores (or models) in whatever shape I want. It also lets me define their property types, like types.string and types.number.

I need a way to access a single instance of the RootStore in any component. The easiest way is to create a custom hook using useMemo. (You can also use React context, but this is simpler and my favorite way to use it.)

I hang onto a reference to the store in _store which makes sure I only ever have one instance of RootStore. (Honestly, I probably don’t even need useMemo here.)

This useStore hook will be accessible by any React component in the app, without needing a Provider or anything else.

Now I’ll convert the DimensionInput component into two components, one for Width and one for Height. I’ll use the new useStore hook to grab the RootStore’s values (width and height) and render those. No props needed!

Back in the store, I need to add a couple of setter actions. In MST, the only place that data can be mutated is in an action. This allows MST to track changes properly.

I can pull these setters into the actions like this, to handle changes to the width and height values.

Now I need a couple of MST “views” to compute the area and perimeter. Views are computed values, so they essentially take data that’s already in the store and present it in a different way. In this case, I already have width and height, so I can calculate the area and perimeter and expose those as views. I’ll use the getter syntax here (get perimeter() {}) to make it so I can access those like any other property.

With those views, I can now make my area and perimeter result components.

Back in the main index.js, I don’t have to pass anything through to the inputs or result components, since they all have access to the RootStore.

I need just one more thing to make this all work — MobX-React-Lite. With it, I’ll make any component that needs to react to changes in the store an “observer”. It’s super easy — just wrap it in the observer() function from mobx-react-lite.

And it works!

While React Dev Tools doesn’t show re-renders when using MobX-React-Lite, it is only re-rendering components that display data that has actually changed. This means it’s much more performant!

You might think … “wait a minute, the previous useState version was simpler.” And it kind of was, for this simple application.

But as your app grows and gets many layers of components deep, you’ll love the convenience of the useStore() hook, the well-defined structure of your RootStore, and the awesome performance of observer().

Here’s the Github repo:

If you have questions or comments, please let me know on Twitter!

Co-founder & CTO @infinite_red. Lutheran, husband, dad to 4, weightlifting. Talking shop.