Using the useReducer Hook

Often times as react developers we are conflicted about which hooks to use to manage the internal state of our component. When to use useState or useReducer. So far in my experience, I have seen so many codebases that make use of useState and for some reasons neglect useReducer. In this article, we are going to explore the use cases of useReducer.

What is useReducer?

useReducer is a react hook for state management. It is an alternative to useState. It takes in a reducer function of type(state, action) => newState, an initialState, and an optional lazy initializer.

Below is an example of state management using useReducer: We are using the popular “Counter” component to demonstrate this.

import * as React from "react";

const initialState = {count: 0};

function reducer(state, action) {
 switch (action.type) {
   case 'increment':
     return {count: state.count + 1};
   case 'decrement':
     return {count: state.count - 1};
   default:
     throw new Error();
 }
}
function Counter() {
 const [state, dispatch] = React.useReducer(reducer, initialState);
 return (
   <>
     Count: {state.count}
     <button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
   </>
 );
}

We are managing the count and displaying the current count on the Counter component above. In my opinion, this seems too much just to manage and display the current count in the Counter component. I could write lesser code with useState to do the exact thing without any performance bottleneck. See Below

function Counter() {
 const [state, setState] = React.useState(0);
 return (
   <>
     Count: {state}
     <button onClick={() => setState((prevState) => prevState - 1)}>-</button>
     <button onClick={() => setState((prevState) => prevState + 1)}>+</button>
   </>
 );
}

As seen above, this is way better than using useReducer because it is easier to maintain since here I am only managing the count in the Counter component.

When should you use the useReducer React hook?:

According to the ReactJS official documentation, useReducer is preferable when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Assuming you have a component where you need to manage the following states:

  • Loading: true/false
  • Data: fetched from an external source

useReducer is perfect for the above scenario because when the data is undefined or being fetched from an external source you would want to keep the loading state as true and after data is fetched, you would want to keep the loading state to false.

Furthermore, as seen above, the loading state depends on the data state to determine its value. Let’s demonstrate this in code:

import * as React from "react";
const initialState = {
 loading: true,
 data: undefined
};

function reducer(state, {type, payload}) {
 switch (type) {
   case 'GET_POSTS':
     return {
       ...state,
       ...payload
     };
   case 'LOADING':
     return {
       ...state,
       loading: payload.loading
     }

   default:
     throw new Error("Invalid Type");
 }
}
function Post() {
 const [{loading, data}, dispatch] = React.useReducer(reducer, initialState);

 React.useEffect(() => {
    const loadData = async () => {
     dispatch({type: "LOADING", payload: {loading: true}})
     const res = await fetch('https://jsonplaceholder.typicode.com/users/')
     const data = await res.json();
     dispatch({type: "GET_POSTS", payload: {loading: false, data}})

   }
   loadData()
 }, []);
 if(loading && !data){
   return <div>Loading...</div>
 }
 if(data && !data.length){
   return "no data found"
 }
 return (
   <div>
     {data.map((item) => (
       <div>
         <p>{`${item.name} - ${item.email}`}</p>
       </div>
     ))}
   </div>
 );
}

The above demonstrates a typical use-case for useReducer. However, you can use useState to manage these states i.e loading and data, but you will have to pay extra attention to your loading state. For example, you would have to make sure you are changing the loading state manually when data has been fetched and updated by the data state updater. See below

const [loading,setLoading] = React.useState(true)
const [data,setdata] = React.useState()
 React.useEffect(() => {
   const loadData = async () => {
     setLoading(true)
     const res = await fetch('https://jsonplaceholder.typicode.com/users/')
     const data = await res.json();
     setdata(data);
     setLoading(false)

   }
   loadData()
 }, [])

Assume that we have other states we want to manage, that depends on fetching data say error state and we would like to display something different to show that an error occurred when fetching our data. We would also have to manually keep track of our error state if we are using useState but with useReducer, we would dispatch the Error type to update our error state. See below:

Using useState, our effect becomes:

React.useEffect(() => {
   const loadData = async () => {
     setLoading(true)
     try{
       const res = await fetch('https://jsonplaceholder.typicode.com/users/')
       const data = await res.json();
       setdata(data);
       setLoading(false)
     }catch(e){
       console.log(e)
       setError(true)
     }
   }
 loadData()
 }, [])

Using useReducer, our effect becomes:

React.useEffect(() => {
   const loadData = async () => {
     dispatch({type: "LOADING", payload: {loading: true}})
     try{
       const res = await fetch('https://jsonplaceholder.typicode.com/users/')
     const data = await res.json();
     dispatch({type: "GET_POSTS", payload: {loading: false, data}})
     }catch(e){
       dispatch({type: "SET_ERROR", payload: {error: true, data: null, loading: false}})
     }

   }
   loadData()
 }, [])

Our states can be any number of things that can depend on our previous states and using useState to manually track and update each state where necessary becomes a daunting task. With useReducer, we can dispatch and update relevant states using different action types.

Conclusion

As we have seen above, useState and useReducercan be used interchangeably to manage our state. However, we have tried to answer the question of when best to useReducer. Here are some rules which I got from Kent . C. Dodds:

  • When it’s just an independent element of state you are managing: useState
  • When one element of your state depends on the value of another element of your state in order to update: useReducer

Furthermore, we can have as many useState as possible in our component when we are dealing with the first rule, that’s no problem but it is advisable to useReducer when dealing with the second rule.

For further research you can check the following resources:

Official React Doc useReucer reference

Should I useState or useReducer?, Kent .C. Dodds