React State Management best practices for 2021 (aka no Redux)
By: Emmanuel Meric De Bellefon
Tags: React, Reactjs, Front End Development, React Hook, Redux

React has been around for a long time and has evolved a lot since then. We went from class components to hooks, and many other libraries popped in in the ecosystem.

Many codebases still do extensive use of Redux while its utility has been questioned by its creator, but there are many other alternatives available in the React library that can make your life much simpler.

Let’s first talk about why did we want to have Redux in the first place.

Why did we want Redux ?

It solves the prop drilling problem

Redux solves the problem known as prop drilling. As stated by the React documentation, if a state needs to be shared by multiple components, the best solution is to lift the state up to the closest common ancestor of these components.

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor. React Official docs

Then we can rapidly realize how this could end up into a big mess when coding large applications.

When coding a feature, a state that lives inside a component may be needed in another component that may be “far away” from it. Lifting the state up to the common ancestor may lead to refactoring every component from the common ancestor down to both components that need the shared state.

Even worth, as your app grows, more and more states will be lifted up and they’ll get closer to the root, which means you’re going to end up with a very complex root that entangles all the states of your app. More and more props will have to be passed from the root to components down in the tree, that’s why we talk about prop drilling.

That’s the main reason why Redux came along. It allows state to be shared easily among components that are not “close from each other” in the tree, in a maintainable and extendable fashion way.

It guarantees state integrity

Take a look at this reducer :

Can you tell if the value of count will remain positive ?

Yes ! you can !

Because here the state can be modified only by two possible actions : INCREMENT and DECREMENT. The first one will simply increment the value, while the initial value is 0. For the second one, we check that the value is not already 0 before decrementing.

So in the end, without even looking at where the reducer is used and how it is used, you can determine that the count value will be always positive. This ensures then that you won’t have any negative count bugs in your app.

Let’s compare this with a local state version :

Can you tell that it will be always positive ? No.

To be sure you will have to look at every place of your app where the setCount is used, and check what value is passed. It may even depend on some props or some other existing states : setCount(aProp) setCount(anotherState-1) . Then you would have to know the lifecycle of these other states to be able to know if your count will ever get negative or not.

In the end, we can say the reducers make the state more predictable and robust.

By limiting how the state can be modified to some specific ways, we increase the predictability and the robustness of our code.

There’s also another advantage of reducers that we must notice. Take a look at this example :

We have a state that is composed of multiple values : isLoading , hasError , data . But we always change all three of them at the same time.

With a reducer, things are going to get much more concise because we just have to dispatch an action each time to change the state :

Why we don’t want Redux ?

It is verbose

Overall Redux is very verbose. To read and update a single store value, you need :

an action constant

an action creator

a case statement in a reducer

a dispatch

a selector

This is much more than calling a single state setter as with a local state, and this is going worse when you start unit testing your code, such as reducers and selectors.

It can lead to performance issues

It is often misunderstood that Redux can be a huge performance bottleneck if not used wisely. To understand that we just need to realize what happens when we dispatch an action :

all reducers are called to produce the “next” store state

to produce the “next” store state all mapStateToProps / useSelectors of mounted components are called

for every mapStateToProps / useSelector that returned a different reference from the previous render, the associated components are rendered

Because of the third point, action dispatch usually ends up re-rendering a lot of components in your application, leading to a lot of code execution, and potential lag.

This particularly means that action dispatching is not good for updates that require an “instant” feedback to the user, such as typing in a text input. Making an action dispatch on each keystroke is very likely to lead to a laggy interface as your app grows.

It doesn’t allow state replication

One big advantage of local state is that it replicates itself when you instantiate a component multiple times. Then each instance has its own state internally.

Let’s take a list of todo items for example. You can toggle each item as “done” or “not done” by clicking on it. The implementation is pretty straightforward using local state :

With a reducer, since it’s going to store the whole list of items, the implementation is going to be more messy :

Also, it’s true that sometimes we need to store the state at the list level because we may need to “access all items at once”. A list input in a form to be submitted to an API is an example.

What do we want instead ?

So we’ve seen Redux's main disadvantages, but we’ve seen also the advantages : solves the prop drilling problem and guarantees data integrity.

The main problem of Redux is that it tries to solve everything at once :

being global to solve prop drilling

using reducers to increase predictability

Then we are “forced” to use global state with Redux which decreases performance and makes state replication impossible, and we are “forced” to use a verbose reducer even when state integrity is not a matter.

The solution : use the same principles, but locally and separately :

global state can be replaced by local contexts

global reducers can be replaced by local reducers with useReducer

And both can be used independently from each other.

Nice ways of using React contexts

React contexts allow to store a value on a component while it’s easily accessible from any child at any level below the component without having to pass a prop at each level.

They are a very nice alternative to Redux, since it’s very rare for a state to be needed to be global. Usually, you just want to declare it at the page level or so :

You can use custom hooks to make things look nicer :

If for any reason you need to access the state from a place that is not under the context, it’s very easy to lift it up. Since there is no prop drilling, we don’t have to update every component like when lifting a state without context.

The only drawback is that it can make some performance bottlenecks if not used wisely. We recommend this article to know more about it.

Essentially, remember not to put too many attributes inside a single context, because as soon as one changes, it will re-render all components that implement a useContext for it, even if they don’t depend on that specific attribute.

As of July 2021, we don’t have yet context selectors in React, which would allow performant contexts with many attributes, but it seems that they’re coming kind of soon.

Guarantying state integrity with useReducer

Thanks to the hook useReducer we can also use reducers for local states. This is not limited to Redux and global state. You can build custom hooks and wrap them in a context exactly like above.

Use it to store complex states with multiple attributes, where data integrity needs to be ensured or when multiple fields change at the same time.