Rethinking React State

Eventually some of your React components will get too complicated to easily maintain. When they do, it can be helpful to split them up into smaller parts. To figure out the right way to do this, we first need to think about what a React component is made of.

Anatomy of a React Component

From the outside, a React component is a black box. You don’t need to know how it’s implemented internally, you just know that it takes some props in and outputs some UI.

When you take a look at the inside though, a component is made of 3 parts. There’s the logic that determines its behavior (the brain), the lifecycle (the heart) and the rendering (the palette).

Anatomy of Containers and Presenters

A common pattern is to split your component into containers (behavior) and presenters (rendering). The lifecycle could exist in either component depending on how you decide to implement it. For this example, we will put it together with the rendering.

This right side of this split works well. React is great for handling rendering and DOM lifecycle. The left side, on the other hand, has some problems. React has a mechanism for handling state, but it’s not ideal. There are 3 key issues with using React for your logic:

  1. Testing is harder because you have to deal with rendering the component
  2. The logic and state is coupled to React
  3. It’s hard to make the state accessible outside of the component

React + Stores

If we could write our logic outside of React, we could solve all of these problems. We just need to put our logic into a regular javascript class, and reference it from our component.

With this structure the first component is completely empty, just referencing the store, so we can just remove it entirely.

This is what we ultimately want, but it’s not quite so simple to achieve it. For our store to work, we need to know what parts of React the brain relies on. There are 2 core React concepts that are relevant to the brain: props and state. Specifically, here’s what we need from our store:

  1. State changes cause the component to be rerendered
  2. Props provided to the component are available to the store
  3. The store can know when props change

If we use mobx for our state we can achieve the first goal. We also get to avoid the use of setState and all of its associated problems. To get access to the props we will need our store to have a reference to the component. Once we have that reference we can easily read the props from it. We can also use mobx to know when the props change. As long as our component is marked as an observer, which is required for our state changes to cause rerenders, the props will also be made observable, meaning we could use mobx reactions to respond to changes in props.

Store Improvements

Ideally, the store should also be able to be created without an associated component. A lot of the time we have logic in our app that is not connected to some particular part of the UI. We may even have cases where we don’t have access to the component until after the store is created. An example might be if we want to create a store and pass it to a component as a prop so we can read its state from other components too. In this case the component needs to be able to associate itself with the store after the fact.

We also may want to configure our stores somehow. React components handle this well by taking in a single object containing key value pairs. We can use the same pattern for our stores. Our new interface for our stores becomes something like this: new Store(props, component). Since now we can receive props through the constructor and through the associated component our store should see its props as the combination of these two.

Show Me The Code

mobx-base-store is a small library that defines a class that you can extend to gain this functionality, as well as some other nice features like propTypes and defaultProps.

To demo it, we’ll use a simple example: a component that fetches some data from an api, does some manipulation of the data, and renders it. We’ll start with the store.

As you can see, this store has no connection to React. A javascript developer with no React experience at all could implement it. It defines all of the behavior of our component, but nothing about how it looks. We could theoretically even write our component with some other UI framework without having to make any changes to this part of the code.

We avoided the use of setState entirely. Manipulating state in the store is as simple as doing a regular assignment operation, and the state changes happen synchronously, potentially reducing bugs. If you’re using mobx already, this is nothing new.

We have access to any configuration (the fetch method) through this.props, just like a regular React component. At this point, exactly where those props come from is not important.

We can also easily test the store without any ceremony around rendering. Notice that we do not need any other libraries like enzyme to write our tests:

Finally, to connect the store up to a component we just add a constructor that creates the store, and then we can use it in componentDidMount and render. We use React for the rendering and lifecycle hooks, but remove all of our logic from it.

In this case, we instantiate the store by calling Store.create(null, this). This means that we are not providing any props to the store explicitly, but it will receive all the props that this component receives. If we wanted to have access to the store in another component, we could easily instantiate the store elsewhere and pass it to the component as a prop.

That’s all. We’ve managed to extract the brain of our component out into a store that is completely distinct from React and hook everything together so our component behaves as expected. Here’s a link to the code sandbox if you want to try it out: https://codesandbox.io/s/7zk0p0jz66

Summary

In my experience, this approach results in much cleaner code than the current recommended approaches. Without doing anything drastically different from a code perspective we have massively improved the testability of our component and reduced our coupling to React.

If you are already using mobx (which I highly recommend) you shouldn’t have to change much about how you write your components. In fact, for most components you should just be able to copy/paste everything over since stores maintain the props interface you’re already using. Maintaining this interface keeps the learning curve of using this technique extremely low.

There’s several other scenarios that mobx-base-store can handle, but we’ll save those for another post. Let me know in the comments if you have any comments or suggestions!