How to Use React's useReducer Hook to Handle State

In this lesson, we'll configure the useQuery and useMutation Hooks we've created to use another state management Hook to handle state. We'll use React's useReducer Hook.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

Table of Contents

This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL with a single-time purchase.

Thumbnail for the \newline course TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL
  • [00:00 - 00:15] Our use query and use mutation hooks work the way we want it to. They return the data we expect from our GraphQL requests and return some status information about our requests, such as the loading and error states of the request being made.

    [00:16 - 00:37] If we take a look at how we're manipulating the state objects in our hook, we can see that we're using the use state hook to achieve this, since use state is one of the primary hooks given to us by React to be able to manage the state of components. The state we're trying to manipulate and track is an object, where each field of the object dictates something about the request.

    [00:38 - 00:51] If we try to explain what we're doing in each setState function in layman terms , we can sort of label each of these functions as actions of sorts. The first action is to essentially set the loading status.

    [00:52 - 01:11] The second action is to essentially set the data, and the last action if ever to occur is to set the error status. Since we have a clear pattern of actions you want, and we're interacting with the same object, we can instead use another state management hook that React provides, called useReducer.

    [01:12 - 01:20] We'll look to first implement the useReducer hook in our use query hook. So we'll import the useReducer hook from the React library.

    [01:21 - 01:41] UseReducer behaves very similar to how Redux works, and it's an alternative to useState. The React documentation state that useReducer is usually preferable to useState when you have complex state logic that involves multiple subvalues, or when the next state depends on the previous one.

    [01:42 - 01:56] UseReducer takes a reducer function that receives the current state and an action and returns the new state. UseReducer returns an array of two values and can take three arguments.

    [01:57 - 02:10] The first argument is the reducer function. The second argument is the initial state, and the third argument, if used, is an initialization function responsible in initializing the state.

    [02:11 - 02:26] So this might still be somewhat complicated if you've never used Redux or understand the flux pattern, so this will be better understood in practice. First, we'll define a simple reducer function outside of our hook.

    [02:27 - 02:42] A reducer function is just a function that receives the current state and an action that will return the new state. Often times, a switch statement is used to determine the return value of state based on the action received.

    [02:43 - 03:03] Action is usually an object that might contain a payload value we can use to update the state with, and usually always contains a type value describing what kind of action is being made. So we'll specify in our switch statements the action type.

    [03:04 - 03:15] And by convention, action types are often denoted with capital letters. Let's specify the different cases and the returns we expect our reducer to take for each action type.

    [03:16 - 03:35] We'll specify three cases that we'll label as fetch, fetch success, and fetch error. And just in case, though this should never happen, we'll specify a default case that throws a blank error.

    [03:36 - 03:53] For each of these cases, we'd want the reducer function to return a new updated state object. We have access to the initial states, and to conform to the Redux flux pattern of how state should be treated immutable, we'll always return new state objects for each case.

    [03:54 - 04:07] For the first fetch action, we simply want to make the loading field of state to be true. So we'll use the spread syntax to simply place the values of state in our new object, and we'll only update loading to true.

    [04:08 - 04:27] In our success state, we'd want to update the data in our state as well as ensure loading and error are false. The new payload that we'd want to apply would arrive from the action itself.

    [04:28 - 04:42] In the error case, we'll want to ensure loading is false, while error is set to true. We haven't specified the types of our state and action objects yet.

    [04:43 - 05:00] We've already stated the interface for state before, so we can use that to type set our state argument in our reducer. We need access to the TData type variable, so we'll say our reducer function will receive a TData type variable as well.

    [05:01 - 05:14] We can create a new type labeled action that represents the different action objects that can pass through our reducer. Each action is an object that contains a type field.

    [05:15 - 05:25] These type fields match that of the case labels we've created in our reducer. Fetch, fetch success, and fetch error.

    [05:26 - 05:47] We know in our fetch success action we expect a payload that has the shape of our data from the server. Similar to our state interface, we'll say the action type expects a TData type variable, which would be used to describe the shape of payload for the fetch success case.

    [05:48 - 06:04] We can then specify the type of the action arguments in our reducer function accordingly. And finally, we can explicitly define the expected type returned from the redu cer function to ensure we're always returning the same object.

    [06:05 - 06:25] Since the reducer function is to return an updated state object, we'll just say its return type is state while passing in the TData type variable. Now with our reducer fully established, let's attempt to use it in a use redu cer hook that we'll specify at the top of our use query hook.

    [06:26 - 06:46] The use reducer hook returns two values in a tuple, the state object itself, in a dispatch function used to trigger an action. The use reducer hook takes a minimum of two arguments, the first being the redu cer function itself, and the second being the initial state.

    [06:47 - 07:18] We'll pass in the reducer function we've created as the first argument, and use the initial state object we specified in our use state hook as the second argument, where data is null, loading is false, and error is false. We could look to try and introduce type variables to the use reducer function itself, however the use reducer hook does a good job in inferring the type of state and dispatch based on the type of the reducer function defined.

    [07:19 - 07:38] If we hover over our state definition, we can see that it's recognized to be of type state, but the type variable being passed in is unknown. This tells us that loading and error are most likely set to be Booleans, but the data type in our object is to be unknown.

    [07:39 - 07:55] Unknown behaves similar to any with some minor differences, and was introduced in TypeScript version three. Like any, anything could be assigned to unknown, however, we cannot access properties or values within an unknown type.

    [07:56 - 08:09] Regardless, we want to type defined data within our state, and it's unknown because we haven't passed in the type variable that the reducer function expects. Here's where we can do something interesting that can help us achieve this.

    [08:10 - 08:25] Instead of passing in the reducer function directly, we can pass in a function that returns the expected reducer function. This will help us pass along the T data type variable that's being passed from our use query hook to the reducer function.

    [08:26 - 08:41] So right above our use reducer hook will create a fetch reducer function that's simply equal to the reducer function while passing in the appropriate type variable, T data. We'll also execute this function.

    [08:42 - 08:57] TypeScript will emit a warning here, since it says if you want to execute the reducer function, you better pass in the expected state and action payloads, and expect the state object to be returned. But that's not what we want.

    [08:58 - 09:05] We want to pass in the entire reducer function above. We don't want the return statement of it, and we don't want to run it.

    [09:06 - 09:28] So in this case, we'll modify our reducer function slightly by being a function that returns the expected function. We can then now pass this newly created fetch reducer function in our hook, and we can verify that data is now appropriately typed.

    [09:29 - 09:37] Now our reducer function is appropriately set up. We can remove the use of the use state hook and where we use the set state function.

    [09:38 - 09:55] In our fetch function, we can now use the dispatch function extracted from the use reducer hook to dispatch actions at every stage of our request call. At the beginning, we'll dispatch the fetch action.

    [09:56 - 10:12] Upon success, we'll dispatch the fetch success action and pass in the appropriate payload, and on error, we'll dispatch the fetch error action. And that's it.

    [10:13 - 10:26] If we head to the browser, we should verify our use query hook works as intended, and our app loads as expected. If we take a look at the use of use reducer, we can see that what we've achieved is very similar to what we had before.

    [10:27 - 10:38] We're simply changing the values of the different fields in our state at different points of our request. The disadvantage with use reducer is it comes with a little bit more boiler plate and understanding.

    [10:39 - 10:53] Its advantages, however, is it's much more preferable to use for complex state objects with multiple subvalues. In addition, it decouples the updates that happen to our state from the actions themselves.

    [10:54 - 11:03] Great. Let's now update our use mutation hook to also use the use reducer hook before we conclude this lesson.

    [11:04 - 11:18] Since the state and actions mimic one another between use query and use mutation, we'll copy information and just make changes as we go. The first thing we'll do in the use mutation file is we'll import the use redu cer hook from React.

    [11:19 - 11:32] We'll then define an action type that's the same as the one in our use query hook. We'll also create a reducer function that is the same as the one in use query.

    [11:33 - 11:48] We'll copy over the fetch reducer declaration and the use of the use reducer hook at the top of use mutation. And finally, we'll specify the different dispatches at the right time.

    [11:49 - 12:06] We'll also remove all the pre-existing code we had that used the use state hook . To verify everything works, we'll first reseed our app to gain more listings.

    [12:07 - 12:22] And by deleting a listing, we can see that our use mutation hook works as intended. The listing is removed and our UI is refreshed to show the updated state of our listings.

    [12:23 - 12:43] As we've copied content from use query to use mutation, it might be very apparent that we can extrapolate a lot of the shared content between these two hooks to a utility file or a helper. The state object, the state interface, the action type, even the fetch API being used.

    [12:44 - 12:57] All of these are either identical or very similar between these two hooks. In our case, we're going to keep it separate and replicate this information just so it appears pretty obvious what each hook does and contains.

    [12:58 - 13:11] However, you're more than welcome to extrapolate some of this identical content to a shared location. With that said, this brings us to the end of this lesson as well as the end of our custom hooks implementation.

    [13:12 - 13:22] In the next module, we're going to discuss some of the potential shortcomings with our custom implementation and what we'll be using instead moving forward.