Managing Complex State with React's useReducer Hook

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
we're going to start this video off probably not where you'd expect by talking about javascripts for each method for each lives on array.prototype and every instance of array has access to it it allows you to invoke a provided function once for each element in an array now say you had an array of numbers we'll say 2 4 and 6 using for each to iterate through each number how would you add all of the numbers together to get a single value which would be 12. one approach might look like this with for each to add up all of the values you need to create and manage an intermediate value here where we're calling state and modify it on each invocation as this demonstrates not only is 4-h dependent on the state of our application but it's also modifying state outside of its own scope this makes it an impure function while not always bad it's best to avoid impure functions when you can to accomplish this same functionality with a pure function we can use javascript's reduce method reduce which is also referred to as fold accumulate or compress is a functional programming pattern that takes a collection usually an array or object in javascript as input and returns a single value as output in javascript the most common use of reduce is the reduce method all arrays have access to the key difference between reduce and for each is that reduce is able to keep track of the accumulated state internally without relying upon or modifying state outside of its own scope that's what makes it a pure function the way it does this is for each element in the collection it invokes a reducer function passing it to arguments the accumulated state and the current element in the collection what the reducer function returns will be passed as the first argument to the next invocation of the reducer and that will eventually result in the final value so we can see this in action by walking through the steps here the very first time the reducer function is invoked state will be zero and the value will be two then on the next invocation state will be whatever the previous invocation returned which was zero plus two and value will be the second element in the array 4. then on the next invocation state will be 6 which was 2 plus 4 and value will be 6. finally since there are no more elements in the collection to iterate over the return value will be 6 plus 6 or 12. so here's what we know so far reduce is a functional programming pattern that takes a collection as input and returns a single value as output the way you get to that single value is by invoking a reducer function for every element in the collection now instead of using this pattern to transform arrays how can we apply it to creating better ui what if instead of our input collection being an array it was a collection of user actions that happened over time then whenever a user action occurred we could invoke the reducer function which would get us the new state so assuming we had a simple ui that was just a button and a counter that incremented every time the button was clicked here's what that flow might look like using the reducer logic initially the ui would show zero and a plus button and then when that button was clicked the reducer function would be invoked state would be zero and the value would be one that would give us our new ui which would be one and then the plus button and we can just follow that same logic every time the button is clicked so it might seem strange but if you think about reduce in the context of being a functional programming pattern it makes sense that you can utilize it to create more predictable uis now the question is how exactly do we do that this brings us to the use reducer hook react comes with a built-in hook called use reducer that allows you to add state to function components but manage that state using the reducer pattern the api for user reducer is similar to what we saw earlier with reduce however there's one big difference instead of just returning the state as we saw earlier we need a way for user actions to invoke our reducer functions because of this use reducer returns an array with the first element being the state and the second element being a dispatch function which when called will invoke the reducer when invoked whatever you pass to dispatch will be passed as the second argument to the reducer which we've been calling value the first argument which we've been calling state will be passed implicitly by react and will be whatever the previous state was so we can see all of this in action with this code so the flow here is the exact same as the diagram we saw earlier whenever the plus button is clicked dispatch will be invoked that will call reducer passing it two arguments state which will come implicitly from react and value which will be whatever was passed to dispatch which in this case is one what we return from reducer will become our new count and finally one count changes react will re-render the component updating the ui at this point you've seen how use reducer works in its most basic form what you haven't seen yet is an example of use reducer that resembles anything close to what you'd expect to see in the real world so to get closer to that let's add a little bit of functionality to our app instead of just incrementing count by one let's add two more buttons one to decrement count and want to reset it to zero for decrementing that is pretty easy all we need to do is come down here and we can add a new button but instead of dispatching one because of math we can just dispatch negative one so we can go up and then we can go down but for resetting the count to zero it gets a little bit trickier right now with how we've set up our reducer function there's no way to specify different type of actions that can occur to update our state we only accept a value which we're getting from whatever we pass the dispatch and we add that to the previous state what if instead of dispatching the value directly we dispatch the type of action that occurred that way based on the type of action our reducer can decide how it wants to update the state so what that would look like in our jsx down here is something like this instead of dispatching one we can dispatch increment and then down here we can say decrement and then we can have a button here where we dispatch reset and then now inside of our reducer we can change how we update the state based on those type of actions so instead of naming this value let's name it action to better represent what's happening and then now what we'll do is let's have an if statement and we're going to have four different clauses here the first one will say if action equals increment then what do we want to do we want to return state plus one if action is decrement then how do we want to modify the state we want to take the state and -1 from it if the action is reset then we want to return 0 and finally if none of our cases match then we just want to throw an error so now we can do is we can increment we can decrement and then we can reset the value or the state to zero this is where we start to see use reduce or shine you may not have noticed it but we've completely decoupled the update logic of our count state from our component we're now mapping actions to state transitions we're able to separate how the state updates from the action that occurred we'll dive into the more practical benefits of that later in this video for now let's add another feature to our app instead of incrementing and decrementing count by one let's let the user decide via a slider so let's come in here and i'm just going to paste the slider component in it's going to take in three props on change which is a function that's going to be invoked whenever the user changes the slider as we see down here min which is the minimum value of the slider and then max which is the maximum value of the slider and then now we can add this to our app down here we can say slider pass it a min which will be 1 a max which will be 10 and then this on change prop which for now we'll just leave empty and then to have some separation here let's just render hr there we go so we see this we now have the slider between 1 and 10 and the way we get the value from the slider again is via this on change prop so knowing this and knowing that it's the value of the slider that will decide by how much we increment or decrement count what changes do we need to make to our reducer so right now the state for our reducer is an integer which represents the count as we can see right here with the initial state then as we can see any time that we return state we're returning an integer this worked previously but now we need our reducer to manage another piece of state for our slider value so instead of being an integer let's make our state an object this way any new piece of state that our reducer needs to manage can go as a property on that object so first let's change our initial state to be an object we'll give it a count which is zero and then we'll give it a step which is going to be the value of the slider we'll start that at one so now that our state is an object we need to update our reducer so instead of returning an integer we want to return an object because that is now the shape of our state this object has two properties on it count which will be whatever state that count was plus one and then we want step to be just whatever it was so state dot step and we'll do the same thing here we return an object count is going to be state.count minus one with step being just whatever it was then here same idea count is going to be zero step is going to be whatever it was and then down here we can still throw the error and then now the very last thing is right here instead of just this just being count this is actually going to be state and then we will say state.count right there so now everything works as it did before but instead of our state being an integer we now have it as an object so that we can stick other properties on it as well specifically our step value so now the question becomes what do we want to dispatch inside of our onchange prop here in order to update the state of our reducer up until this point we've been able to dispatch the type of action that occurred whether it's increment decrement or reset that worked fine but we're now running into its limitations along with the action type we also need to include some more data specifically in our case we need to pass along the value of the slider so we can add it to our state value specifically in our case we want to pass along the value of the slider so we can update our step state so to do this instead of having our action we dispatch be a string again we've seen this before let's change it to be an object with a type property so that way we can still dispatch based on the type of action that occurred but we can also pass along any other data as properties on the action object so we can see this perfectly by what we dispatch from our on change prop so not only do we want to dispatch the type of action which occurred which is going to be update step but we also want to pass along the value of the slider as well so we'll pass it along as the step property so at this point there are three changes we need to make to our reducer the first change is we need to account for our new action type update step the second is we need to account for changing action to be an object instead of a string and then finally the third change we need to make is we need to update increment and decrement to adjust the count based on the step property and not just one so let's go make those so action is no longer just going to be a string it's going to be an object with a type property so let's change all of these next we need to add the case for our new action type which is going to be update step so we'll say if action.type equals update step then what we want to do is keep count the same as it was previously but step is going to be whatever action.step is which will be whatever we passed right here so now the very last thing we need to do is instead of incrementing and decrementing count by one we want to update it based on whatever step is so here state.count plus state.step and then same thing here state.count minus state.step so now we can still increment and decrement by 1 if we want to but if we come up here and change this to 5 notice now that we can increment and decrement by 5 or by 10 or we can even reset the value still now with that we see another subtle but powerful benefit of use reducer you may have missed because the reducer function is past the current state as the first argument it's simple to update one piece of state based on another piece of state in fact i'd go as far as to say whenever updating one piece of state depends on the value of another piece of state you'll want to use the use reducer hook over use state so in our example we can see this in how we're updating count based on the value of step so at this point we've seen how use reducer works and some of the advantages that it gives us but now let's dive a little bit deeper into those advantages and answer the question you've probably most likely been asking so fundamentally use state and use reducer accomplish the same thing they both allow us to add state to function components so now the question becomes when should you use one over the other well the first benefit of use reducer is it allows you to write more declarative state updates so imagine we were creating a component that was responsible for handling the registration flow of our app in this app we needed to collect three pieces of information from the user their username email and password and for ux purposes we'll also need a few other pieces of state like loading error and registered so let's build this out using the ustate approach as i just mentioned we'll need to add some pieces of state to our component so we'll have username which is an empty string initially we'll also have email which will also be an empty string password and then now for ux reasons let's have a loading value here that's going to be a boolean we'll initially set it to false and then again for ux purposes let's have an error value and then finally let's have a registered value so we can know when we want to change the ui this will initially be false so now let's have a little function here we'll call it handle submit whenever this is invoked i'm going to say prevent defaults and then we want to set loading to be true we want to set error to be an empty string just in case we had an error previously and then you'll notice this new user function up here it's kind of our function we can invoke for creating a new user so we're going to invoke it passing it username email as well as password that's going to return us a promise so first if the promise resolves successfully then what we want to do is set loading to be false set the error to be an empty string this might be a little bit redundant since we have it up here but we'll keep it as anyway and then we want to set registered to be true and if there is an error then what we want to do is set loading to be false and set the error to be whatever the error was and now i'm going to just paste in the rest of the gsx for this component because it's not really super relevant to what we're talking about all we have is if registered is true then we redirect to the dashboard floating is true we show loading and then we have the error and then our very nice form here so now we can enter an email username password and then we can click on submit now first there's nothing wrong with this code it works just fine however it's a pretty imperative approach to solving the problem we're conforming to the operational model of the machine by describing how we want to accomplish the task instead of what if we took more of a declarative approach so instead of describing how we want to accomplish the task let's describe what we're trying to accomplish this declarative approach will allow us to conform closer to the mental model of the developer to do this we can use use reducer so the reason use reducer can be more declarative is because it allows us to map actions to state transitions as we saw earlier this means instead of having a collection of set x and vocations we can simply dispatch the action type that occurred then our reducer can encapsulate all of the imperative instructional code so to see what this looks like let's assume we've already set up our register reducer and we're just updating our handle submit function right here so now instead of having all of these set whatever invocations what we can do is we can replace this with dispatch with the type being what we want to do not how which is login and then we can get rid of these and then we can just dispatch type which is going to be success or in our catch case down here we dispatch the type which is going to be error and we can pass along that error string so we can even make these one line now so notice that we're describing what we want to do log in and then based on the result of that success or error so now we need to build out our reducer and again just for the sake of brevity here i'm just going to paste it in so above our component here we will have our register reducer we can log in success error and then we can also update the input fields with this input action type all of the logic that we had before is encapsulated inside of these cases so if we log in the state remains the same loading goes to true and error goes to an empty string on success loading is false error is going to be string registered as true on error loading is false and error becomes whatever action.error is and then now the only thing we need to do is we can get rid of all of these because all the state is living inside of our reducer we can create a brand new state here using use reducer we pass through it two arguments the first argument is going to be the reducer itself which we called the register reducer and the second argument is the initial state which i created a object for right here and then now all of these are coming from the states we could probably destructure that if we wanted to this is going to be state.error as well as state.error and then all of the values are going to be the exact same coming from [Music] state so now the ui is the same the functionality of our app is the same but again use reducer and the idea that our reducer logic is encapsulated inside of our reducer allows us to have more declarative code and describe what we want to do rather than how we actually want to do it so the next benefit of use reducer is it allows us to more easily update the state of our app based on the previous state and we've already seen this one in action from earlier because the reducer function is past the current state as the first argument it's simple to update one piece of state based on another piece of state in fact i'd go as far as to say whenever updating one piece of state depends on the value of another piece of state reach for use reducer and we can see this in the next benefit too and that use reducer allows us to minimize the dependency array of use effect so part of mastering the use effect hook is learning how to properly manage its second argument the dependency array leave it off and you could run into an infinite loop scenario forget to add values your use effect depends on and you'll have still data add too many values and your effect won't be reinvoked when it needs to be so it may come as a surprise but use reducer is one strategy for improving the management of the dependency array the reason for this goes back to what we've mentioned a few times now use reducer allows you to decouple how the state is updated from the action that triggered that update so in practical terms because of this decoupling you can exclude values from the dependency array since the effect only dispatches the type of action that occurred and doesn't rely on any of the state values which are encapsulated inside of the reducer so we can see that in the differences between these two codes here in the second code block we can remove count from the dependency array since we're not using it inside of the effect so when is this actually useful well here's another code block you notice anything wrong with this code every time count changes which is going to be every second our old interval is going to be cleared and a new interval is going to be set up that's not ideal instead we want the interval to be set up one time and left alone until the component is removed from the dom to do this we have to pass an empty array as the second argument to use effect again as we see here use reducer allows us to do that we no longer need to access count inside of our effect since it's encapsulated in the reducer this allows us to remove it from the dependency array and make it so the interval is set up once rather than every time count changes so for the record there is a way to fix the code without using use reducer you may remember that you can pass a function to the updater function you state gives you when you do this the function will be past the current state value and you can utilize that to clear out the dependency array now this works fine but there is one case where it starts to fall apart if you remember back to our counter component from earlier the final piece of functionality we added was the ability for the user to control the step via the slider component once we added step count was then updated based on that step state this is the use case where our code right here starts to fall apart by updating count based on state we've introduced a new value into our effect which we have to add to our dependency array now we're right back to where we started anytime step changes our old interval is going to be cleared and a new interval is going to be set up again not ideal luckily for us the solution is the same as we saw before with use reducer notice the code is still the same as we saw earlier encapsulated inside of the increment action is the logic for count plus step again since we don't need any state values to describe what happened we can clear everything from our dependency array so in summary you state and use reducer both allow you to add state to function components use reducer offers a bit more flexibility since it allows you to decouple how the state is updated from the action that triggered the update typically leading to more declarative state updates if different pieces of state update independently from one another things like hovering selected etc you state should work just fine but if your state tends to be updated together or if updating one piece of state is based on another piece of state go with the use reducer hook rather than the use state hook hey what's up youtube if you enjoyed this video i think you'll really enjoy our react hooks course it contains pretty much everything you'd ever want to know about react hooks including basic topics like use state and use effect as well as more advanced topics like managing complex state with use reducer as well as performance benefits with used memo and with use callback it has 14 different topics around 4 hours worth of video over 25 000 words of text 13 different exercises you'll work through 10 different quizzes as well as two production ready projects that you will build so if you want to learn react hooks i really believe in the reviews back this up that there's no better way on the internet than with our course so if that sounds interesting go to ui.dev slash react hooks and you can check it out
Info
Channel: uidotdev
Views: 7,199
Rating: 4.9863482 out of 5
Keywords:
Id: XUxQSzpgAuI
Channel Id: undefined
Length: 25min 4sec (1504 seconds)
Published: Mon Nov 23 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.