DEV Community

Cover image for React useReducer Hook Explained: When to Use It Over useState
Shefali
Shefali

Posted on • Originally published at shefali.dev

3

React useReducer Hook Explained: When to Use It Over useState

React useReducer hook is a powerful alternative to the useState hook for managing complex states efficiently. If your state logic involves multiple conditions or dependencies, useReducer provides a structured way to handle it.

In this post, you’ll learn about the useReducer hook, including its syntax, how it works, and the difference between useState and useReducer.

Before we get started, don’t forget to subscribe to my newsletter!
Get the latest tips, tools, and resources to level up your web development skills delivered straight to your inbox. Subscribe here!

Now, let’s jump right into it!🚀

What is useReducer?

useReducer is a React Hook that helps manage state using a functional approach.

Instead of directly updating the state (like useState), it uses a reducer function that receives an action and decides how to update the state. It follows a dispatch-action pattern, similar to Redux.


When to use useReducer over useState?

  • If state logic is simple, useState is enough.
  • If state updates depend on previous values, useReducer is better.
  • If multiple state updates happen together, useReducer keeps things structured.

Basic Syntax of useReducer

Before using useReducer, you need to import it from React:

import { useReducer } from "react";
Enter fullscreen mode Exit fullscreen mode

Syntax:

const [state, dispatch] = useReducer(reducerFunction, initialState);
Enter fullscreen mode Exit fullscreen mode

Here,

  • reducerFunction(state, action): A function that handles the logic of updating the state.
  • initialState: Initial value of the state.
  • dispatch(action): Triggers state updates based on an action.

useState vs useReducer Comparison (Counter App)

To understand useReducer better, let’s first build a simple counter app using useState.

Using useState:

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

How does it work?

  • useState is used to store the count value.
  • The buttons update the state directly using setCount( ).

Now, let’s see how you can achieve the same using useReducer.

Using useReducer:

import { useReducer } from "react";

// Reducer function
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
      <button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

How does it work?

  • The reducer function determines how the state changes based on the action type.
  • Instead of setCount( ), you call dispatch( ) with an action.

Complex State Management (Todo App)

If you build a Todo App in which multiple states (add, delete, toggle complete) are updated, then the useReducer hook is better than useState.

Todo App Using useState:

import { useState } from "react";

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState("");

  const addTodo = () => {
    setTodos([...todos, { text: input, completed: false }]);
    setInput("");
  };

  const toggleTodo = (index) => {
    setTodos(
      todos.map((todo, i) =>
        i === index ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const removeTodo = (index) => {
    setTodos(todos.filter((_, i) => i !== index));
  };

  return (
    <div>
      <h2>Todo App using useState</h2>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={addTodo}>Add Todo</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
            <button onClick={() => toggleTodo(index)}>Toggle</button>
            <button onClick={() => removeTodo(index)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;
Enter fullscreen mode Exit fullscreen mode

How does it work?

State Management:

  • todos: An array storing todo items ({ text, completed }).
  • input: Stores the current value of the input field.

Adding a Todo:

  • Creates a new todo object with { text: input, completed: false }.
  • Updates the state using setTodos([…todos, newTodo]).
  • Clears the input field after adding a todo.

Toggling Todo Completion:

  • Uses .map() to iterate over todos.
  • Finds the todo by index and updates its completed status (true ↔ false).
  • Returns a new array with updated values (immutability).

Removing a Todo:

  • Uses .filter() to create a new array excluding the selected todo.
  • Updates the todos state with the filtered list.

Todo App using useReducer:

import { useReducer, useState } from "react";

function todoReducer(state, action) {
  switch (action.type) {
    case "add":
      return [...state, { text: action.payload, completed: false }];
    case "toggle":
      return state.map((todo, index) =>
        index === action.index ? { ...todo, completed: !todo.completed } : todo
      );
    case "remove":
      return state.filter((_, index) => index !== action.index);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [input, setInput] = useState("");

  return (
    <div>
      <h2>Todo App using useReducer</h2>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={() => dispatch({ type: "add", payload: input })}>Add Todo</button>
      <ul>
        {todos.map((todo, index) => (
          <li key={index} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {todo.text}
            <button onClick={() => dispatch({ type: "toggle", index })}>Toggle</button>
            <button onClick={() => dispatch({ type: "remove", index })}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;
Enter fullscreen mode Exit fullscreen mode

How does it work?

Reducer Function (todoReducer):

  • Handles state updates based on action types.
  • Uses a switch statement to determine how the state changes.

Action Types & Their Effects:

  • “add”: Adds a new todo ({ text: action.payload, completed: false }).
  • “toggle”: Finds the todo by index and toggles its completed state.
  • “remove”: Filters out the todo at the given index.

Using useReducer Hook:

const [todos, dispatch] = useReducer(todoReducer, []);
Enter fullscreen mode Exit fullscreen mode
  • todos stores the list of todo items.
  • dispatch(action) triggers state updates via todoReducer.

Handling Input & Actions:

<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={() => dispatch({ type: "add", payload: input })}>Add Todo</button>
Enter fullscreen mode Exit fullscreen mode
  • Input is managed using useState.
  • dispatch({ type: “add”, payload: input }) adds a new todo.

Rendering Todos:

{todos.map((todo, index) => (
  <li key={index} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
    {todo.text}
    <button onClick={() => dispatch({ type: "toggle", index })}>Toggle</button>
    <button onClick={() => dispatch({ type: "remove", index })}>Remove</button>
  </li>
))}
Enter fullscreen mode Exit fullscreen mode
  • Maps over todos to display each task.
  • “Toggle” button updates the completion status.
  • “Remove” button deletes the todo.

You can see, with multiple state updates (add, remove, toggle todos), how useReducer simplifies the logic. Instead of multiple setState calls, a single dispatch(action) updates the state predictably. This makes it easier to scale and debug compared to multiple useState calls.

How useReducer help?

  • Centralized logic for handling state.
  • Avoids unnecessary re-renders caused by multiple useState updates.
  • Scales well as your application grows.

useReducer vs useState: When to use?

Feature useState useReducer
State Complexity Best for simple state Best for complex state
Multiple State Updates This can create confusion in multiple state update Actions and reducer make it easy
Performance Fast for small state Optimized for large & complex state
Use Case Basic counters, form inputs, UI state Forms, Todos, complex logic, state dependency

Best Practices & Common Mistakes with useReducer

  • Use the useReducer if multiple state changes are dependent on each other.
  • Define the type for every action to make debugging easy.
  • Use useReducer only when the state is complex; don’t take this as a replacement for useState in every case.

Mistake: Using useReducer for every small state

const [state, dispatch] = useReducer(reducer, false);
dispatch({ type: "toggle" });
Enter fullscreen mode Exit fullscreen mode

This is a simple toggle, so useState is a better option for this.

Mistake: Not Using Action Payloads for Dynamic Updates

dispatch({ type: "addTodo" }); // No data passed!
Enter fullscreen mode Exit fullscreen mode

Solution: Pass a payload with the necessary data. This makes reducers more flexible and reusable.

dispatch({ type: "addTodo", payload: "Learn useReducer" });
Enter fullscreen mode Exit fullscreen mode

🎯Wrapping Up

That’s all for today!

For paid collaboration connect with me at : connect@shefali.dev

I hope this post helps you.

If you found this post helpful, here’s how you can support my work:
Buy me a coffee – Every little contribution keeps me motivated!
📩 Subscribe to my newsletter – Get the latest tech tips, tools & resources.
𝕏 Follow me on X (Twitter) – I share daily web development tips & insights.

Keep coding & happy learning!

Top comments (0)