DEV Community

Cover image for React useReducer
Lucius Emmanuel Emmaccen
Lucius Emmanuel Emmaccen

Posted on

React useReducer

Table of content

  1. Introduction

  2. The useReducer Syntax
    2.1. What is a state in React
    2.2. What is a dispatch in React
    2.3. The Reducer function
    2.4. Initial state
    2.5. Lazy initialization

  3. How the useReducer hook works

  4. Bailing out of a dispatch

  5. useReducer with Typescript

  6. useState Vs useReducer

  7. When to use useReducer

  8. When not to use useReducer

  9. Recommend videos

  10. Interesting reads

  11. Conclusion

Introduction

State management has been a common topic in the development world over the years. It's a challenging part of the development process, especially for huge applications. This challenge has been solved in many different ways over time and keeps evolving in positive ways.

In this article, we're going to learn from grassroots, what there is to know about the useReducer hook, how it helps you manage application state better (and logic), along with real-world examples of how it works and why you might want to use it in your next or current project.

The useReducer Syntax

const [state, dispatch] = useReducer(reducer, initialArg, init);

The useReducer is a function that takes up to three arguments and returns a state and a dispatch. These three arguments are used to determine what the state is and how the dispatch function works.

useReducer hook by Lucius emmanuel
Don't worry about understanding this upfront, we'll go through every inch of what this means and how it works.

What is a state in React?

const [state, ...

A state in react is a piece of data that represents the current status of our application. This state can be used to provide information on the screen or perform background computations/calculations that allow our application to function. State is a fundamental idea in React.
Here's a visual example of a state in React, holding some information about a user.

States in useReducer by Lucius emmanuel

What is a dispatch in React?

const [state, dispatch...

Dispatch in React is simply a function that takes an instruction about what to do, then passes that instruction to the reducer as an "action". In simple terms, see dispatch as a friendly boss that likes to tell the reducer what to do and is happy about it 😎

the reducer function by Lucius emmanuel

If you're familiar with Redux, this term dispatch might not be new to you but, we'll go through this article assuming you're new to both Redux and useReducer.

The reducer function

const [state, dispatch] = useReducer(reducer, ...

The reducer function is a special handler that takes in two parameters. The application's current state and an action object, then it uses that to determine/compute the next state of our application (it returns a new state).
Remember how we talked about the dispatch telling the reducer what to do by passing it an action πŸ’­?. These actions usually contain a type (what to do) and a payload (what it needs to do the job).

Here's a typical example of what a reducer function looks like:

function reducer(state, action) {
  switch (action.type) {
    case "FIX_BUGS":
      return { totalBugs: state.totalBugs - 1 };
    case "CREATE_BUGS":
      return { totalBugs: state.totalBugs + 1 };
    case "SET_TOTAL_BUGS":
      return { totalBugs: action.payload };
    default:
      throw new Error();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we're starting to connect the dots as we'll go through what all these mean visually.

the use reducer function by Lucius emmanuel

Let’s delve into what’s happening in the switch statement. In a real-world application, you’d most likely have more complex logic in the switch but for this example, we’ll be keeping it simple.

We have three cases in our switch statement, which are case "FIX_BUGS", case "CREATE_BUGS" and case "SET_TOTAL_BUGS". These are the actions we're explicitly trying to handle. The default only fires when we dispatch an action that doesn't match any of our cases. See cases (or action types) as a store of all possible things a particular reducer can do. If the reducer were a person, "cases" would be the skill list on the CV.

Here's a visual representation of how the dispatch tells the reducer what to do.

dispatch actions paired with reducer by Lucius emmanuel

The initial state

const [state, dispatch] = useReducer(reducer, initialArg, ...

The initial state in a react useReducer function is the starting state of our application e.g. The default state. In the example we used earlier, we're setting the total bugs of our application in the reducer function based on what type of action the dispatch tells us. For this reason, we can set the starting point or default state of our bugs count to say... "0" πŸ€”? or maybe even "100"? πŸ›.

Let's do a hundred! 😎 {totalBugs : 100}

Lazy initialization

const [state, dispatch] = useReducer(reducer, initialArg, init);

The useReducer takes in an optional third parameter we'll call init. This init is going to be a function we'll pass as the third argument to useReducer. This can be useful if we'd like to create the initial state lazily.

A common use case would be a situation where the initial state needs to go through some calculations to arrive at a default state or has to fetch data from an API, etc. It looks something like this in code:

const init = (initalState) => {
  console.log(initalState); // 100

//... do some code magic πŸͺ„
//...

  return inital;  // return desired state

};
Enter fullscreen mode Exit fullscreen mode

How the useReducer hook works

Now that we've gone through the syntax: const [state, dispatch] = useReducer(reducer, initialArg, init); from left to right, it's time to start putting together the pieces in code.

Here's the complete code snippet for our bugs count application.

import { useReducer, useState } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "FIX_BUGS":
      return { totalBugs: state.totalBugs - 1 };
    case "CREATE_BUGS":
      return { totalBugs: state.totalBugs + 1 };
    case "SET_TOTAL_BUGS":
      return { totalBugs: action.payload };
    default:
      throw new Error();
  }
}
const init = (inital) => {
  console.log(inital); // 100
  return inital;
};

export default function App() {
  const [state, dispatch] = useReducer(reducer, { totalBugs: 100 }, init);

  // creating a new state so we don't add extra in the reducer state
  // (this is just for examples)
  const [inputState, setInputState] = useState(0);

  return (
    <div className="App">
      <h1>useReducer</h1>
      <p>{state.totalBugs} Bugs Left πŸ§‘β€πŸ’»</p>
      <button onClick={() => dispatch({ type: "FIX_BUGS" })}>FIX_BUGS</button>
      <button onClick={() => dispatch({ type: "CREATE_BUGS" })}>
        CREATE_BUGS
      </button>
      <input
        onChange={(e) => setInputState(+e.target.value)}
        value={inputState}
        type="number"
      />
      <button
        onClick={() =>
          dispatch({ type: "SET_TOTAL_BUGS", payload: inputState })
        }
      >
        SET_TOTAL_BUGS
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

For a live preview of the working code. I created a sandbox.

From the walkthrough of the useReducer syntax and what each bolt and knot means, to seeing the actual code in action. We can denote that the useReducer is a function that can take up to three arguments and returns a state and a dispatch.

Here's a simple and summarized visual representation of the useReducer lifecycle in code.

useReducer lifecycle by Lucius emmanuel

Bailing out of a dispatch

If you return the same state in your reducer hook as the current state. React is smart enough to bail out of rendering or firing effects on components depending on that state. This is because React uses the Object.is comparison algorithm and this tells React that nothing has changed.

useReducer with Typescript

useReducer is no stranger to us anymore but how do we actually use it with TypeScript πŸ’­?. Now if you're already familiar with TypeScript, this one should be a piece of cake, however, if you're fairly new, don't worry, we'll have a quick and simple example to get us going.

What we're going to do here is by no means a rule, you're free to use any formula or paradigm that makes you sleep better at night πŸ˜‰.

Here it is:

import { ChangeEvent, useReducer } from "react";

// for our state
interface State {
  firstName: string;
  lastName: string;
  age: number;
  language: string;
}

// list of all possible types
enum ActionTypes {
  UPDATE = "UPDATE",
  RESET = "RESET"
}

// action type
interface Actions {
  type: ActionTypes;
  payload?: {
    key: string;
    value: string;
  };
}

// if we need to do some work to provide initial state
const init = (inital: State) => {
  console.log(inital);
  return inital;
};

// the default state of our app
const initialState = {
  firstName: "",
  lastName: "",
  age: 0,
  language: ""
};

// the reducer handler function
const userFormReducer = (state: State, action: Actions) => {
  switch (action.type) {
    case "UPDATE":
      return { ...state, [action.payload.key]: action.payload.value };
    case "RESET":
      return initialState;
    default:
      throw new Error();
  }
};

export const UserForm = () => {
  const [state, dispatch] = useReducer(userFormReducer, initialState, init);

  // for input change event
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    dispatch({
      type: ActionTypes.UPDATE,
      payload: { key: event.target.name, value: event.target.value }
    });
  };

  return (
    <div>
      <h1>TypeScript Example</h1>
      <input
        value={state.firstName}
        type="text"
        name="firstName"
        onChange={handleChange}
        placeholder="first name"
      />
      <input
        value={state.lastName}
        type="text"
        name="lastName"
        onChange={handleChange}
        placeholder="lastName"
      />
      <input
        value={state.language}
        type="text"
        name="language"
        onChange={handleChange}
        placeholder="language"
      />
      <input
        value={state.age}
        type="text"
        name="age"
        onChange={handleChange}
        placeholder=""
      />
      <button
        onClick={() => dispatch({ type: ActionTypes.RESET })}
        type="reset"
      >
        RESET
      </button>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

To test this out yourself, live on Sandbox. I've also included the typescript example.

This article is not about TypeScript, so we won't be explaining what's going on in that code. A few things to note however is that I intentionally put all the TypeScript code in one file. In a real-world application, you'd most certainly create files for different things and your approach might be quite different (Again, whatever helps you sleep better at night πŸ˜‰).

useState Vs useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
//vs
const [state, setState] = useState(initialState)
Enter fullscreen mode Exit fullscreen mode

We won't be creating a webinar or zoom meeting to argue about this, but a rule of thumb is, that everything you can do with a useReducer you can do with a useState. In fact, the useReducer hook is just an alternative to useState with a few clear differences such as:

  • Readability
  • Ease of use and,
  • Syntax

Note πŸ’‘
React guarantees that dispatch function identity is stable and won’t change on "re-renders". This is why it’s safe to omit from the useEffect or useCallback hook dependency list.

When to use the useReducer hook

use a useReducer hook over useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. This is fairly common when your application begins to grow in size. useReducer also lets you improve performance for components that trigger deep updates because you can pass down a dispatch instead of callbacks.

When not to use useReducer

useReducer is awesome, but there are certain scenarios where it doesn't make our lives any easier. Here are a few of such cases:

  • Don't use a useReducer hook when you're dealing with simple state logic.
  • When your application needs a single source of truth. You'll be better off using a more powerful library like Redux
  • When prop-drilling starts to yell at you. This happens when you get trapped in the hellish world of passing down too many props (state) to and from child components to a child component that later comes to hunt you.
  • When state lifting to parent/top-level components no longer suffices.

Recommend videos

There's always more to learn, so if you'd like to learn more tricks and hacks about the useReducer hook. I'd recommend you look up the video links below

More interesting reads from my blogs

Conclusion

The useReducer is a powerful hook if used properly and I hope we've been able to learn about it without any confusion. If you'd like to contribute to this post or drop feedback, feel free to leave a comment πŸ“ and drop a like ❣️. Thanks for reading, you're awesome! 😎

Happy Coding!

I'd love to connect with you πŸ™‚

PS: This article was initially published on "Copycat"

Top comments (0)