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.
The useReducer Hook#
๐ This lesson's quiz can be found - here.
๐๏ธ Solutions for this lesson's quiz can be found - here.
๐ Grab a cheatsheet describing how React'suseReducer
Hook can be used - here.
Our custom useQuery
and useMutation
Hooks work the way we want them to. They return the data we expect from our GraphQL requests and return some status information such as the loading and error states of our requests.
If we take a look at how we're manipulating the state objects our Hooks are returning, we can see that we're using the useState
Hook to achieve this. This is because useState
is one of the primary Hooks given to us by React to manage the state of functional components.
// useQuery
export const useQuery = <TData = any>(query: string): QueryResult<TData> => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false,
error: false
});
// ...
};
For both the useQuery
and useMutation
Hooks, the state we're trying to manipulate and track is an object where each field of the object dictates something about the request. Every function in our fetch that sets the state can be seen to be an action of sorts.
The first action sets the loading status to
true
.The second action sets the data in
state
to the data received.The last action if ever to occur is to set the error status to
true
.
Since we have a clear pattern of actions that interact with the same object, we can instead use another state management Hook that React provides called useReducer
.
useReducer
#
We'll look to first use the useReducer
Hook in our custom useQuery
Hook, so we'll import the useReducer
Hook from the react library in the useQuery.ts
file.
import { useState, useReducer, useEffect, useCallback } from "react";
useReducer
behaves very similar to how Redux works.
Redux is a library that adapts the flux pattern to managing state in a client-side application.
useReducer
takes the concepts of Redux and allows us to manage data with a similar pattern!
The useReducer
Hook 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:
The first argument is the
reducer()
function.The second argument is the initial state.
The third (optional) argument is an initialization function responsible for initializing the state.
const [state, dispatch] = useReducer(reducer, initialArg, init);
The useReducer
Hook will appear more understandable when we start to implement it.
reducer
#
In the useQuery.ts
file, we'll define a simple reducer()
function outside of our Hook. A reducer()
function is a function that receives the current state and an action that would return the new state.
import { useState, useReducer, useEffect, useCallback } from "react";
import { server } from "./server";
// ...
const reducer = (state, action) => {};
export const useQuery = <TData = any>(query: string): QueryResult<TData> => {
// ...
};
A switch
statement is often used to determine the return value of state based on the action received.
import { useState, useReducer, useEffect, useCallback } from "react";
import { server } from "./server";
// ...
const reducer = (state, action) => {
switch () {}
}
export const useQuery = <TData = any>(
query: string
): QueryResult<TData> => {
// ...
};
The action
parameter of the reducer
function is to be an object that might contain a payload value we can use to update the state with. action
is to usually contain a type
property describing what kind of action is being made. action.type
will be the expression used in the switch
statement to evaluate the returned new state
object in the reducer
function.
import { useState, useReducer, useEffect, useCallback } from "react";
import { server } from "./server";
// ...
const reducer = (state, action) => {
switch (action.type) {
}
};
export const useQuery = <TData = any>(query: string): QueryResult<TData> => {
// ...
};
By convention, action types are often denoted with capital letters. Let's specify the cases and the returns we expect our reducer to take for each action type. We'll specify three cases - FETCH
, FETCH_SUCCESS
, and FETCH_ERROR
.
Though this should never happen, we'll also specify a default
case in our switch
statement that will throw an error if the action.type
does not exist or match either the FETCH
, FETCH_SUCCESS
, or FETCH_ERROR
types.
import { useState, useReducer, useEffect, useCallback } from "react";
import { server } from "./server";
// ...
const reducer = (state, action) => {
switch (action.type) {
case "FETCH":
return; // ...
case "FETCH_SUCCESS":
return; // ...
case "FETCH_ERROR":
return; // ...
default:
throw new Error();
}
};
export const useQuery = <TData = any>(query: string): QueryResult<TData> => {
// ...
};
For each of the cases in our switch
statement, we'd want the reducer to return a new updated state object. We have access to the initial state
as the first argument of the reducer()
function and to conform to the Redux/Flux pattern of how state should be treated immutable (i.e. can't be changed), we'll always return new state objects for each case.
Our
useReducer
Hook is to interact with a state object similar to what we had before. The state object contains thedata
,loading
, anderror
fields.
For the first FETCH
action that is to be fired, we simply want to make the loading
field of state
to true
. We'll use the spread syntax to place the values of state
in our new object and update the loading
property to true
.
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL