React State: Choose Wisely
Let’s look at 3 ways of managing state in react.
Originally published at Pony Foo.
There are plenty of options for managing state in a React app. But there’s very little guidance about which one you should use in any situation.
Let’s fix that.
The solution you pick to manage state should fit they way you want to use the items you hold in state. Your choice should also make it easy for you to update, reuse, and refactor your code. In other words, your choice of state management can make your life easy or hard.
Here’s a non-exhaustive list of things you should consider:
- How quickly can you add new state code?
- How well encapsulated is your stateful data?
- How easy is it to pull the data into a new component?
Let’s use this list as a basis for comparing different ways of handling state. To do this, we’ll make the same simple component using three different forms of state handling. By making the same thing three ways, you’ll get a few nice perspectives.
First, you’ll see that any approach can solve any problem. This is important because if you know that you can solve nearly any problem with any form of state management, you can turn your attention from the technical problem of getting it working, to the more abstract problem of making it work well in your over all application.
Second, by making the same thing three ways, you’ll start to see the subtle differences which can become big headaches as an applications grows.
What are the three ways to manage state?
- Local State: State stored directly on the component.
- Global State: A global store. In this example, you’ll use redux.
- Context: A newer API that lets you access state higher up the tree.
Using the three points of comparison and the three ways of managing state, you’ll be able to build a nice table that will let you compare the different options.
Alright. Let’s get started.
You’ll be making a very simple counter. It’s just two things: the current count and a button to add to the current count. As mentioned you’ll have one for each type of state, but they all act exactly the same way.
So far, nothing too exciting. The first thing you are going to do is make the presentation layer. This will be a simple reusable component that displays the count and the button. Making this independent is not only a good practice, it will also a show you how easy it is to swap different forms of state handling.
Now that you have your basic display component, you can start to build the stateful components.
The first component you’ll build will store state locally, directly on the component.
All you need is a basic class with
state as a property and a method to add to state. Pass both the
count from the state object and the
addOne() method into your presentation component and you’re all done.
At this point, one thing should jump out: it’s easy. Even without knowing much about other forms of state handling, you can see this one is very simple to set up. But it gets even better.
With the introduction of React Hooks, you’ll remove nearly all the boilerplate code.
Here’s an updated version using react hooks that’s nearly identical to the official docs:
count is the stateful data, and
add() is a method for updating state. It would really be difficult to make this easier.
The next component you’ll build will use a global store. In this example, you’ll use redux since it’s by far the most popular, but many of the issues would be the same if you were using other options like MobX or even Apollo and GraphQL.
We’ll move quickly past some of the complexities of implementing redux. If you need a refresher, check out the official docs.
To start make an action to add an item to the global store.
Next, make a reducer to actually store the count.
If you’ve worked with redux before you may have ignored this part, but it’s important to take a moment to think about what you just did here.
You just created a global state object with the a property of
count . That property name is now unique for this particular action. If you wanted to create another property with a similar name you’d need to either give it a unique name such as
clickCount or encapsulate one, the other, or both inside a nested object.
You’ll return to this idea in a bit. For now, use
connect to pull the state into your display component.
Again, we won’t explore the details, but as a summary, the component is tapping into the global store so that you can both read from it, using
mapStateToProps, and update it using
mapDispatchToProps. Other than that, your display component doesn’t care where the data comes from.
So far, the thing that should jump out immediately is how much work is involved. There are more files, more imports, more complexity. That’s not a problem if the advantages outweigh the extra work — and they often will — but it is still something to consider.
The final component will use context. This is a fairly new form of state handling that was introduced in React 16.3. Context is similar to local state in many ways, but it has a slightly different implementation.
To start off, you need to create a context. This will set up a couple defaults that you will use when you create an implementation.
This sets the initial state of
0 and adds a noop function that you will override later.
To actually use context, you will first need to make a Provider. A provider is the base component that will hold the state object and the methods for updating the state.
If you scroll up, you’ll notice this looks very similar to the local state component. The big difference is that you are combing the state and the update function in single object which you then pass down into a Provider component that wraps everything else. In this case, you are wrapping
this.props.children but you can also wrap a component directly.
To use this component, you’d wrap some other components. In this case, you’ll wrap a single component.
Notice, you are not passing any props to
OtherComponent . More clearly, you are not explicitly passing anything to
OtherComponent. The provider is holding the state information so that you can tap into it later.
Even if you go several components deep, you will still be able to access the provider’s state. So if
OtherComponent returns another component:
AnotherComponent returns yet another component. That information is still hanging in the background.
Ok, that’s far enough. It’s time to pull out the state and do something with it.
To get the information you need to create a Consumer. The consumer uses render props to pull out that single value object. Remember,
addOne . You can use destructuring as a short hand.
Now that you have the state and the function to update state, you can pass it into your display component:
As you saw, any form of state handling can do the same thing. In reality, if you put effort into it, you can make any state handling system work for any piece of data in your application.
Of course, who wants to put effort into it? The goal is always less effort, better results. With that in mind, it’s time to compare the different approaches.
This is the easiest comparison. Nothing beats local state particularly when you use react hooks. It’s quick, it does that job and it’s easy to refactor into a more complicated system if necessary. As a rule, you should always start with local state. If you need something more complicated, you can refactor as you go.
For this reason, Local State wins for simplicity.
Next, let’s explore how well the data is encapsulated. If data is well encapsulated, you can reuse a component multiple times without any concern that another component may accidentally change your state. Encapsulation isn’t necessarily a goal in itself. There are times where you absolutely want different components to access and modify shared data (more on that in the next section).
As with most things, it’s about predictability. If you expect a component to be independent, the data shouldn’t be open to modifications by other components.
To explore encapsulation, you’ll need to add multiple versions of a component to see how they interact.
To start off return to the original page that has all three components.
The code looks something like this:
You have a simple parent component that wraps one instance of each component. To test encapsulation, all you need to do is add a second instance.
Start off with the
StateCounter component. The updated code looks like this:
When you open that in the browser, you can easily see that the data is well encapsulated. Every time you click the add button, it only updates that count on that particular component.
Now try adding a second redux component.
Open this in the browser and notice how different it is. Every time you click on Add , you update the data on both components.
In this case, these components are not independent. You can alter the data in any component that use the same action.
Now, there are certainly ways around this. You can have an
id on the global store for each component. You can use an array. You can use a different namespace. But the fact is that global stores are best for global data. Encapsulation is not a primary goal.
How about context? Well, this is were things get a little more tricky. What does it mean to add a second component? Does that mean you add a second consumer? In this case, that would mean adding a second
OtherComponent under the same provider. Or do you add a second
Why not both? First, add another provider. Then in the existing provider, add a second instance of
Let’s see what happens.
When you click on the first component, the counter iterates, but the other counters do not. But when you click on the second context counter, it will change the third counter. Similarily, if you click on the third counter, it will change the second.
This means that context is somewhere in the middle. It can be encapsulated when you want it to be, and it can be global when you want it to be.
Now, this claim comes with a big caveat. The ability to share date or keep it private all depends on how you order the components. Anything in the same hierarchy as the provider will share data, anything outside will not. This means that you may have to move providers up or down depending on how you want to use the consumers.
Still, at this point, you can finish filling out the chart. Local state is well encapsulated and context can be. They each get a check. Global state is not well encapsulated to it does not get credit.
That just leaves one more area of concern: availability. The beauty part is that you don’t need to write any more code. Sometimes you want your data to be easy to access.
Consider a shopping cart. Pushing users to a sale is the most important thing your code can do. Consequently, nearly every part of your app will potentially need to know about the cart (how much is in it, the total, etc). This is data you want to make easily accessible.
In this case, you can use what you learned above. Local state, for example, could work, but you would have to store it so far up the component hierarchy that you’d be passing it down a lot of props.
Global state, on the other hand, was designed specifically for this. It’s an easy 👍.
Context as you saw above can either be global (at least depending on where you put it in the hierarchy) or encapsulated. Go ahead and give it credit.
The final comparison looks like this:
You may look at this table and think that local state or context are somehow better. That’s not true. The best solution is the one that works best for your data and your app.
One thing that’s not reflected in this chart is the large collection of third-party code that ties in with a global store. If you want to use observables with your store, then you’re better off using redux-observables than trying to roll your own solution.
Each of these solutions exist for a reason. Your job is to find the fit that makes your code easiest to build and maintain.