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:
- Testing is harder because you have to deal with rendering the component
- The logic and state is coupled to React
- It’s hard to make the state accessible outside of the component
React + Stores
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:
state. Specifically, here’s what we need from our store:
- State changes cause the component to be rerendered
- Props provided to the component are available to the store
- 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
reactions to respond to changes in props.
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
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.
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
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
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!