useReducer Hook

Learn when to use React useReducer hook instead of useState for better state management in complex components.

Loading...
useReducer Hook

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.

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";

Syntax:

const [state, dispatch] = useReducer(reducerFunction, initialState);

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;

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;

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;

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;

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, []);
  • 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>
  • 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>
))}
  • 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" });

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!

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

dispatch({ type: "addTodo", payload: "Learn useReducer" });