From useState to useReducer: A Better Approach for Complex State Logic

Valentin Nagacevschi
7 min readFeb 19, 2023

--

If you’re into React or React Native development, whether you’re a beginner or an expert who’s moved on from classes and into functional components, chances are you’re already familiar with the useState hook.

This is probably the most essential hook in a React developer’s toolkit and it’s used in at least half of all built components for good reason. With this hook, you can manage a component’s state and, together with props, control how and when the component is rendered.

We use useState all over our codebase and it never lets us down. But sometimes, we need a better way. It really depends on the situation. You’re probably fine if you have one or two instances of useState in your component. But the code can become difficult to read and understand if you have three or more.

For example, imagine a component that handles a set of inputs whose values need to be collected and saved (like a settings screen). When you have multiple useState hooks, keeping track of what’s going on can quickly become confusing.

So, what’s the solution? One option is to use a custom hook to handle multiple states in a more organized way. This can make your code slightly easier to understand and maintain. With a custom hook, you can group related state variables and their update functions, reducing the clutter in your components, but in our case, we just moved the problem elsewhere. Additionally, the custom hook will need to return the separate states and the required methods to update them. Not so much helpful.

Hey, can we do better than this? Absolutely! And guess what, we can do it without using the typical Redux approach that involves dispatching actions. Instead, we can use a less-known hook called useReducer which is readily available in React. But hold on, you might be wondering, how is this hook simpler than the trusty old useState?

Well, let me explain. First, let’s take a look at what useReducer does and then determine if it can help us. As per React documentation, useReducer is not a fundamental hook, but rather an additional one, which means we don’t need to worry too much about it. The documentation states that “useReducer is a React Hook that lets you add a reducer to your component”. And it goes on to say…

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.

But what if our state logic is straightforward, and our next state update only affects the user interface? Can we still use useReducer? Let’s see what we can do with it. From the documentation, we can see what the code looks like.

Now we need to figure out two things, how dispatch and reducer work. Let’s see.

How does dispatch works?

Calling this dispatch function, you can easily update the state to a new value, which will then cause the component to re-render. For that, you have to pass one argument to the dispatch function called an action. Now it gets a bit confusing, but let’s see what that action is. Usually, we saw this pattern when using dispatch:

An action is just an object we are passing to the reducer method when we call dispatch. What happened if this action is nothing else but the state, or even better, a partial state? Well, this state or partial state is what we are going to receive in the reducer callback method. Let’s keep this in mind when we’re going to bring all these together.

How does reducer work?

The reducer callback method specifies how the state will be updated. It receives the current state and the action as arguments and it should return the updated state. This method must be pure, in the sense that it will return a newly updated state, made from the arguments and without any side effects. Most important for us, the state and action arguments can be of any type. This means that a possible useful reducer for us can be:

This reducer will update the state by merging the old state with the new state which was passed as action. This is great.

Bringing everything together

Let’s try now to rewrite the whole useReducer hook in the way it suits our needs.

Now you start to see where I’m going, isn’t it? Let’s move on with the initial example and build the initialState.

With the initialState done, how are we going to use it? It’s as simple as this:

To be honest, I think I prefer better this new way. It reminds me of the good old time when we all used classes and we have only one method to update the state, setState and the local state was one big happy object. If it made sense back then, it makes sense today.

Not to mention that you can update the state with more than one partial, like this, which comes so naturally.

It’s great, isn’t it? Now we have one unified way to update the local state and if we want to add a new member to the state object, we don’t have to wonder about the name of the corresponding method to update it. We’ll keep using setState.

Let’s turn this into a custom hook we’re going to save into a custom-hooks file and then import it all over our project.

And use it in this way.

The good part, if you’re using plain JavaScript, is that you can call setState with any object, thus adding it to the local state, even if not present in the original initialState.

Bonus: Typescript

Everything looks great in plain old JavaScript but we are all living in a world where TypeScript is taking over. For instance, the latest (0.71) version of ReactNative is using TypeScript by default. How can we have the new custom hook useSuperState in TypeScript? Let’s see.

But that’s not enough. You also need to type the initialState members and also make them all optional since we should be able to set them individually. Using Partial on typeof will do the trick most of the time.

Gotcha: If you have initially undefined members, you should still add them to the initialState and type them accordingly to use them safely.

Conclusion

We all love using useState in our React and React Native apps, right? It’s straightforward and gets the job done most of the time.

But let’s face it, when we start using it excessively, it can be a real headache to read and understand the code.

Even moving it to a custom hook doesn’t always make things clearer. That’s where useReducer comes in as a saviour! It may have a bad rep because of our collective disdain for Redux, but it can be a lifesaver when it comes to managing complex state logic or multiple state variables.

Plus, we can even turn it into a custom hook with a name that makes more sense to our specific use case (like useSuperState, for example).

Using useReducer can make our code easier to read and understand while keeping all the related state variables together. So give it a shot and see how it works for you!

I hope you found this article useful, and I’d love to hear your thoughts in the comments. And if you enjoyed it, give it a round of applause or share it with your pals. Thanks for reading, and stay tuned for more great content!

--

--