Implement useState with useReducer

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 3 years ago
Updated 2 years ago

A great way to learn how hooks work is to try and implement them yourself. In this video we'll learn about both of the useState and useReducer hooks by reimplementing useState with useReducer.

This is based on my blog post: "How to implement useState with useReducer"

Instructor: [00:00] Here we have a simple counter component. Every time I click on this, that count increments. I want to re-implement useState using useReducer. What I'm going to do here is, I'll make a function called useState, and we're going to swap that.

[00:17] Then, this useState is going to take our initial values. We'll say, initial value. We're going to return react.useReducer.

[00:29] We're going to need to provide a reducer, so I'm going to make a function called useStateReducer. It's going to be my useState reducer. We'll paste that right in there.

[00:39] Luckily for us, useReducer returns an array, so that destructuring is going to be the same. We want to create the exact same API, so that's going to work out nicely for us. With useState, it is the state value, and then a function that will directly update the state. With useReducer, it is the state value and a function.

[00:59] With useReducer, it is the state value and a function that, when called, will call the reducer. Whatever is returned from the reducer, will be the new state.

[01:04] The way that this is being called right here is with a function. I'm going to simplify this really quick, and we'll restore it later. I'll copy this, paste it, and we'll do a simplified form, count +1, rather than the function update form. We'll support that later.

[01:21] What we're going to get here in our reducer, is the previous state and the dispatchArg. What we want to do is, basically we want to make the new state be whatever is passed to our dispatch function. I'm going to say, dispatchArg. That's our new state.

[01:39] Then, the API for getting the initial state value set is a little bit different with useReducer than with useState. With useState, you can provide the actual value, or a function that returns the actual value.

[01:52] With useReducer, though, you provide an initial value, and then you also provide an initializer function. We'll call that a useStateInitializer.

[02:03] Let's go ahead and make that function, useStateInitializer. What this function is going to do, is it's going to take the initial value here, and it's going to return what the initial value for the state should be. We're going to accept an initial arg, and we'll return that initial arg.

[02:20] Actually, with that and the way that we're using useState right now, we've successfully re-implemented useState with useReducer, we're just missing a couple of features. Let's go ahead and follow the code flow, so we get an understanding of what's going on here.

[02:34] We call into useState, useState takes an initial value. We call into useReducer, that's going to pass our useStateReducer, which is responsible for creating the new version of state any time our state updater is called, our dispatch function.

[02:48] UseReducer is also called with the initial value and a useStateInitializer function. The useStateInitializer function will accept that initial value that we pass as the second argument here, and return what the initial state should be for this particular instance of useReducer.

[03:04] In our case, we're going to say whatever the initial value is, is going to be our initial state. That is how we get our count that initializes at zero, and our setCount that updates the state to be whatever we pass to our dispatch function.

[03:19] Let's go ahead and support this version of calling the dispatch function, where you can actually call it with a function that returns the new state. Now, dispatchArg is a function that needs to be called.

[03:29] If I click on Click Me, we're going to notice we get the count no longer rendering, and we'll get a console error because we're trying to render a function, because we set our state to this function. All we need to do here is, we can say, typeof dispatchArg is a function.

[03:44] If it is, then we'll call dispatchArg with the previous state. Otherwise, we'll return the dispatchArg.

[03:53] Now we support both versions of the API. We can prove that here if we restore this, that we had before, count +1, save that, and we get the incrementing. If we switch to that new API, we also get the incrementing there as well.

[04:09] Another feature that useState supports that we should add a support for, is lazy initialization, where we can pass a function that returns our initial value. This can be useful if you're reading from local storage or something.

[04:21] We're not supporting that right now. You'll notice we're not rendering zero here anymore, because we're trying to render a function, so we're getting that error.

[04:27] All we need to do is something similar here. We say, typeof initial arg is a function, then we're going to call it. Otherwise, we'll return it.

[04:39] With that now, we're supporting both of those use cases. Let's go ahead and copy that, comment this one out, and return the zero. All of these use cases for useState are now supported in any combination, because we're doing all of that checking.

[04:54] In review, this is one way that you can implement useState with useReducer, by providing a reducer that accepts the previous state and the dispatch argument, so whatever our dispatch function is going to be called with.

[05:05] If our dispatch function happens to be called with another function, then we'll simply call that function with the previous state. Otherwise, we'll return the value, making certain that our dispatch function resembles the state updater function from useState.

[05:18] Then, for the initialization, we're going to pass the initial value that's provided to us, and a state initializer function, which will accept that initial value. If that initial value is a function, then we'll simply call it. Otherwise, we'll return that initial value to be the initial value of our state.

[05:34] Now, the useState that we've created has the exact same API as the useState that comes baked in with React.