React useReducer Hook

  • useReducer is a React hook designed to handle complex state logic. It is often preferred over useState when your state transitions are sophisticated or when the next state depends heavily on the previous one.
  • It follows a pattern similar to Redux: you dispatch "actions" to a "reducer" function, which then determines how to calculate the next state.
Developer Tip: Think of useReducer as a way to "decouple" the logic of how the state changes from the component that triggers the change. This makes your UI code cleaner and easier to test.

Purpose

  • Managing Complex State: Well-suited for state objects that contain multiple sub-values or nested properties.
  • Predictable Transitions: By centralizing state updates in a single reducer function, you ensure that state transitions are deterministic and easier to debug.
  • Logic Reuse: Because the reducer is just a standard JavaScript function, you can even move it into a separate file to keep your component files small and focused.
Best Practice: Use useReducer when you find yourself having 3+ useState calls that often update together, or when your state logic is getting hard to follow inside a useEffect.

Syntax

  • Import: Pull useReducer from the main 'react' package.
  • The Reducer Function: This function takes two arguments: the state (where you are now) and the action (what happened). It must return the new state.
  • The Hook: Call useReducer(reducer, initialState). It returns an array containing the current state and a dispatch function used to trigger updates.
import React, { useReducer } from 'react';

// 1. Define initial state
const initialState = { count: 0 };

// 2. Define the reducer function
const reducer = (state, action) => {
  // We use a switch statement to handle different action types
  switch (action.type) {
    case 'ACTION_TYPE':
      return { ...state, someValue: action.payload }; 
    default:
      // Always return the current state if the action type is unknown
      return state;
  }
};

// 3. Initialize in your component
const [state, dispatch] = useReducer(reducer, initialState);
Common Mistake: Forgetting to return a value from your reducer! If you don't return anything (or return undefined), your state will vanish. Always include a default case that returns the current state.

Counter

  • A counter is the "Hello World" of useReducer. It demonstrates how a single action can transform a simple number.
import React, { useReducer } 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 };
    case 'RESET':
      return initialState;
    default:
      throw new Error('Unknown action type');
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    

Example: Counter

Count: {state.count}

{/* Dispatching an action object */}
); };
Watch Out: State in React is immutable. In your reducer, never do state.count++. You must always return a new object (e.g., { count: state.count + 1 }).

Todos

  • Managing a list is a perfect real-world scenario for useReducer. Here, we handle adding items and toggling their completion status using a payload.
import React, { useReducer, useState } from 'react';

const initialState = [];

const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      // Return a new array containing the old items plus the new one
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      // Map through items and flip the 'completed' boolean for the matching ID
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
};

const Todos = () => {
  const [todos, dispatch] = useReducer(reducer, initialState);
  const [newTodo, setNewTodo] = useState('');

  const handleAdd = () => {
    if (newTodo.trim() !== '') {
      // We pass the input text as the 'payload'
      dispatch({ type: 'ADD_TODO', payload: newTodo });
      setNewTodo('');
    }
  };

  return (
    

Example: Todo List

setNewTodo(e.target.value)} placeholder="What needs to be done?" />
    {todos.map(todo => (
  • dispatch({ type: 'TOGGLE_TODO', payload: todo.id })} style={{ cursor: 'pointer', textDecoration: todo.completed ? 'line-through' : 'none' }} > {todo.text}
  • ))}
); };
Developer Tip: The payload property is a convention used to pass extra data to the reducer. While you can name it anything, "payload" is the industry standard.

 

Summary

While useState is great for simple toggles or strings, useReducer provides a more robust architecture for complex state management. It centralizes your business logic, makes state updates predictable, and scales beautifully as your components grow. By mastering this hook, you are preparing yourself for larger applications and gaining a deeper understanding of the functional programming patterns used in libraries like Redux.