From Chaos to Order: Organizing State with useReducer ๐ŸŽช

From Chaos to Order: Organizing State with useReducer ๐ŸŽช

ยท

7 min read

Why do we use UseReducer if we have UseState?

So up until now, we have been using the useState hook to manage all our state, right? However, as components and state updates become more complex, using useState to manage all state is, in certain situations, not enough. For example, some components have a lot of state variables and also a lot of state updates that are spread across multiple event handlers all over the component or maybe even multiple components. And so this can quickly become overwhelming and hard to manage. It's also very common that multiple state updates need to happen at the same time so as a reaction to the same event. For example, when we want to start a game, we might have to set the score to zero, set an is playing status, and start a timer. And finally, many times, updating one piece of the state depends on one or more other pieces of state, which can also become challenging when there is a lot of state. And so, in all these cases, useReducer can help. So these are the problems that reducers try to solve, so let's see how.

So we call useReducer with a reducer function and its initial state, and it returns a state and a dispatch function. So starting from the beginning, when we use useReducer, we usually store related pieces of state in a state object returned from the useReducer hook. Now, it could also be a primitive value, but usually, we use objects. Now, as we already know, useReducer needs something called a reducer function to work. So this function is where we place all the logic that will be responsible for updating the state, and moving all state updating logic from event handlers into this one central place allows us to completely decouple state logic from the component itself, which makes our components so much cleaner and so much more readable. So when we manage state with useReducer, it's ultimately this reducer function that will be updating the state object. So in a way. It's a bit like the setState function in useState but with superpowers. Now in practice, the reducer is simply a function that takes in the current state and an action and, based on those values, returns the next state, so the updated state. Now, keep in mind that the state is immutable in React. This means that the reducer is not allowed to mutate the state, and in fact, no side effects are allowed in the reducer at all. So a reducer must be a pure function that always returns a new state.

And again, based on the current state and the received action. And speaking of the action, the action is simply an object that describes how the state should be updated. It usually contains an action type and a so-called payload which is input data. And it's based on this action type and payload that the reducer will then determine how exactly to create the next state. And now, the final piece of the puzzle is this. How do we actually trigger a state update? Well, that's where the dispatch function comes into play. So useReducer will return a so-called dispatch function which is a function that we can use to trigger state updates. So instead of using setState to update the state, we now use the dispatch function to send an action from the event handler where we're calling dispatch to the reducer. And as we already know, the reducer will then use this action to compute the next state. Okay, so these are all the pieces that need to fit together to effectively use the useReducer hook. So an action object, a dispatch function, a reducer, and a state object.

Let's compare the mechanism of useReducer with a much simpler useState mechanism. So when we use useState, we get back a setter function, and let's call it setState. And then, when we want to update the state, we pass the new updated state value that we want, and React will update the state, which will trigger the re-render. So it's a lot simpler and more straightforward than useReducer, but since useReducer solves the problems discussed before, it's a great choice in many situations, even though it's a bit more complicated to set up.

Here is a really helpful analogy that made all this really clear to me when I first learned about this. So imagine that you needed to take $5,000 out of your bank account for some reason. Now, since this is a large amount, you can't just do it from an ATM, so you need to go physically to a bank. Now, once you are at the bank, how do you actually get those $5,000? Do you walk straight into the bank's vault, grab the cash, and then go home? Well, I don't think so, right? That's usually not how it works. How it does work is that you go into the bank, and there you'll find a person sitting at a desk ready to assist you. Now, when you arrive at the bank, you already know how much cash you want to withdraw and from what account number. And so you walk right to the person and tell them you would like to withdraw $5,000 from account 923577, for example. What happens then is that usually, the person will type something into his computer, check if you actually have the cash in your account, and if so, he goes to the bank's vault and gets the money to hand it over to you finally. It's your money, after all, right? But note the big difference between this real version and the previous version of the story, where you just grabbed the cash yourself. In this real version, you told the person what to do and how to do it, and he then got the money for you on your behalf, so you didn't take the money directly. And that's a huge difference. So does this maybe start to sound familiar? Well, I hope it does.

And so, let's now bring this analogy back to useReducer and identify what each of these pieces represents in the useReducer mechanism. And let's start with the most important thing, the state. So what do you think the state is in this analogy? Well, the state is represented by the bank's vault because this is where the relevant data, so the money, is stored and also updated. So the vault is what needs to be updated, and so that's our state. Nice, so with that out of the way, let's consider how the money is taken from the vault. So how is the state actually updated? So starting from the beginning, what do you think the customer going to the bank represents in this analogy? Well, the customer going to the bank and requesting the money is clearly the dispatcher because it is who is requesting the state update, right? And they're doing so by going to the person and requesting to withdraw the $5,000. So what is the reducer here, and what is the action? Well, the reducer will be the person working at the bank because that's the one who actually makes the update. In this case, it's the one who goes to the vault to get your money. But how does the person know how much money to take and from what account? They know because you told him so exactly in your request message. And so that request message is clearly the action. In this example, the action can be modeled like this. With the action type being withdrawn and the payload is the data about the withdrawal that you want to make.

To summarize, you went into the bank with a clear action in mind. You then dispatched that action to the reducer, so to the person working there who took the action and followed the instructions to take the right amount of money from your account. So from the state. He then gave you your money finishing this cycle. So you did not go directly into the vault and take your money. Instead, you had the person, as a middleman, who knows better than you how to perform different actions on the vault. So he knows how to deposit, how to withdraw, how to open and close an account, how to request a loan and more. And he does all this without you having to worry about the details. So exactly like a reducer function, which also decouples and abstracts all the state-updating logic away from you so you can have clean and easy-to-understand components.

Did you find this article valuable?

Support The lean Programmer by becoming a sponsor. Any amount is appreciated!

ย