react emote

React

From useState hook to useReducer

How to migrate your code from multiple "useState" hooks to one "useReducer"

2022-12-30
11 min read
Difficulty
react
typescript
react emote

Introduction

The React useReducer hook is one of the less used and under rated built-in hook but it can be especially useful when we use several useState hooks in a component that are related to each other.

TypeScript and 'useReducer'

Many authors have written a lot of interesting articles about this topic and I suggest you to read the Kent C. Dodds post "Should I useState or useReducer? " to get more info about it. You can also find a lot of info in the React Beta Documentation too.

However these tutorials are often written by using React and JavaScript so I wrote this post as introduction for another article about useReducer and TypeScript:

React 18.x & TypeScript | How to safely type the useReducer hook

Topics

multiple useState hooks

Let's imagine that we have to create a simple React component that needs to fetch data from a REST API, show a loading message and display an error if the HTTP request fails.

Furthermore we also want to remove an user from the list, always handling errors and loading messages.

The first example uses 3 different useState to handle all the info we need to control the User Interface :

  • users (Array): store all data fetched from a REST API (a list of users);
  • pending (Boolean): used to display a loading message. This state is set to true when a new request is done and to false when the request completes or fails;
  • error (Boolean): used to display an error message. It's true when the request fails, otherwise must be false.

TIP

There are better solutions to fetch and handle data in React application. You can use Tantask (aka React Query), the latest version of React Router and many other techniques. Anyway in this article I would like us to focus on differences between useState and useReducer

Here the full example: read the comments I wrote into the snippet to understand how it works:

ReactJavaScript
import axios from 'axios';
import React, { useEffect, useState } from 'react';

export const Demo1MultipleUseState = () => {
  // state initialization
  const [users, setUsers] = useState([]);
  const [pending, setPending] = useState(false);
  const [error, setError] = useState(false);

  // when the component is mount
  useEffect(() => {
    // set the pending state
    setPending(true);
    // fetch data from a REST API
    axios
      .get('https://jsonplaceholder.typicode.com/users')
      .then((res) => {
        // success: 
        setUsers(res.data); // save the response in "users" state
        setPending(false);  // remove pending message
        setError(false);    // remove any previous errors
      })
      .catch((e) => {
        setError(true);     // set error
        setPending(false);  // remove pending
      });
  }, []);

  function deleteHandler(id) {
    setPending(true);
    // remove the element invoking the endpoint
    axios
      .delete(`https://jsonplaceholder.typicode.com/users/${id}`)
      .then((res) => {
        // remove the element from the array
        setUsers((s) => s.filter((u) => u.id !== id));
        setError(false);
        setPending(false);
      })
      .catch((e) => {
        setError(true);
        setPending(false);
      });
  }

  return (
    <div>
      <h1>1. Multiple useState Demo</h1>

      {pending && <div>loading...</div>}
      {error && <div>some errors</div>}

      <ul>
        {users.map((u) => (
          <li key={u.id}>
            {u.name}
            <button onClick={() => deleteHandler(u.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

several useState and performance

The previous snippet would have generated multiple component renders before React 18 since we have set multiple useState at the same time after an async operation.

But that's not a problem anymore since now multiple useState operations are batched.

Move to one useState only

The previous snippet works fine but the code becomes less and less readable as the number of useHooks increases and are related each other.

So we can replace all states with just one useState by using an object so structured:

ReactJavaScript
{ 
  users: [], 
  pending: false, 
  error: false 
};

Now we can update the snippet as shown below. Read the comment for more info:

ReactJavaScript
import axios from 'axios';
import React, { useEffect, useState } from 'react';

// NEW: state initialization
const initialState = { users: [], pending: false, error: false };

export const Demo2UniqueUseState = () => {
  // UPDATE: we have just one useState now 
  const [data, setData] = useState(initialState);

  useEffect(() => {
    // UPDATE: set "pending" value
    setData((s) => ({ ...s, pending: true }));

    axios
      .get('https://jsonplaceholder.typicode.com/users')
      .then((res) => {
        // UPDATE: now we use just one useState to update all properties
        // to populate "users" and set both, "error" and "pending"
        setData((s) => ({
          users: s.users.filter((u) => u.id !== id),
          error: false,
          pending: false,
        }));
        
      })
      .catch((e) => {
        // UPDATE: update error and pending properties
        setData((s) => ({ ...s, error: true, pending: false }));
      });
  }, []);

  function deleteHandler(id) {
    setData((s) => ({ ...s, pending: true }));
    axios
      .delete(`https://jsonplaceholder.typicode.com/users/${id}`)
      .then((res) => {
        setData((s) => ({
          ...s,
          users: s.filter((u) => u.id !== id),
          pending: false,
        }));
      })
      .catch((e) => {
        setData((s) => ({ ...s, error: true, pending: false }));
      });
  }

  return (
    <div>
      <h1>2. Unique useState Demo</h1>

      {data.pending && <div>loading...</div>}
      {data.error && <div>some errors</div>}

      <ul>
        {data.users.map((u) => (
          <li key={u.id}>
            {u.name}
            <button onClick={() => deleteHandler(u.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

Tip: Spread operator

What is this syntax?

ReactJavaScript
✅ { ...s, pending: true }

The three dots ... is known as spread operator and it's used to shallow clone or merge all data from an object or an array.

It's very useful in this scenario (or when we need to update just one or few object's props) since I can avoid to manually set all properties to their previous values:

ReactJavaScript
❌ { users: state.users error: state.error, pending: true }

useState with function

Why do I pass a function to useState:

ReactJavaScript
setData((s) => ({ ...s, pending: true }));

instead to simply use the state reference?

ReactJavaScript
setData({ ...data, pending: true });

The useState hook returns a function that receives the current state as parameter. Although it's not really necessary in this example, you should always use this approach when your new state depends from the previous one in order to avoid inconsistent or outdated values.

useReducer

The useReducer hook allow you to create a function that defines how the state should be updated: this function is known as reducer.

Redux

You've probably heard of Redux before, that is a global state manager for React applications, often used in enterprise projects.

Redux is a 3rd party library, but we may also define it as a pattern, based on the concept of reducers. So if you learn how to use the useReducer hook you'll also know one of its fundamental concepts.

The useReducer hook accepts three parameters:

  • (required) A reducer function that handle state updates
  • (required) The initial state
  • (optional) Another way to initialize your state but it's not often used. Read the doc for more info about it.
ReactJavaScript
const [state, dispatch] = useReducer(reducerFn, initialState);

It always returns the current state and a dispatch function that will be useful to notify the reducer when the state should be updated:

ReactJavaScript
dispatch('increment')
dispatch('decrement')
dispatch('doSomething')

Although an action can be a simple string, by convention it should be an object with a type property (the action identifier) and a payload (that contains optional parameters you can send with the action):

ReactJavaScript

dispatch({ type: 'increment', payload: 10 })            // increment by 10
dispatch({ type: 'decrement', payload: 5 })             // decrement by 5
dispatch({ type: 'doSomething', payload 'anything' })

How useReducer works:

  • the state is handled by a (reducer) function
  • the state can be updated only when the dispatch is invoked
  • the reducer is automatically invoked after every dispatch
  • the reducer always receives the current state and the action object ({type: ..., payload: ...})
  • the state can be updated accordingly with the action type and its payload

1. Define the reducer and initialize the state to 0 (zero):

ReactJavaScript
const [state, dispatch] = useReducer(myReducer, 0);

2. dispatch an action to increment the state by 10

ReactJavaScript
dispatch({ type: 'increment', payload: 10 }) 
ReactJavaScript
function myReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return state + 1;
    case 'decrement':
      return state - 1;
  }
  // we always must return the current state in case we dispatch 
  // an action that is not handled by the reducer. Otherwise you'll lose the state value.
  return state;
}

move to "useReducer"

Finally we can migrate our previous code to use the useReducer hook:

ReactJavaScript
import axios from 'axios';
import React, { useEffect, useReducer } from 'react';

// state initialization
const initialState = { users: [], pending: false, error: false };

// NEW: the reducer function
function demoReducer(state, action) {
  // check the action type
  switch (action.type) {
    case 'setPending':
      // set the pending state to true
      return { ...state, pending: true };

    case 'setError':
      // set the error state to true and remove pending
      return { ...state, error: true, pending: false };
    
    case 'loadUsers':
      // store the payload into the state and remove pending
      return { ...state, users: action.payload, pending: false };

    case 'deleteUser':
      // remove the element that has the ID we have sent as payload from the array
      return {
        users: state.users.filter((u) => u.id !== action.payload),
        error: false,
        pending: false,
      };
    
  }
  return state;
}

export const Demo3UseReducer = () => {
  // NEW: set the reducer and initial state
  const [data, dispatch] = useReducer(demoReducer, initialState);

  useEffect(() => {
    // NEW: dispatch the pending action
    dispatch({ type: 'setPending' });
    axios
      .get('https://jsonplaceholder.typicode.com/users')
      // NEW: dispatch load users to store all received data into the state
      .then((res) => dispatch({ type: 'loadUsers', payload: res.data }))
      // NEW: dispatch the error action
      .catch(() => dispatch({ type: 'setError' }));
  }, []);

  function deleteHandler(id) {
    dispatch({ type: 'setPending' });
    axios
      .delete(`https://jsonplaceholder.typicode.com/users/${id}`)
      .then(() => dispatch({ type: 'deleteUser', payload: id }))
      .catch(() => dispatch({ type: 'setError' }));
  }

  return (
    <div>
      <h1>3. Unique useState Demo</h1>

      {data.pending && <div>loading...</div>}
      {data.error && <div>some errors</div>}

      <ul>
        {data.users.map((u) => (
          <li key={u.id}>
            {u.name}
            <button onClick={() => deleteHandler(u.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

"useReducer" and TypeScript

As I said, I have created this article as introduction to the next one:

React 18.x & TypeScript | How to safely type the useReducer hook

I really highly recommend to read it if you're using TypeScript!

Keep updated about latest content
videos, articles, tips and news
BETA