React useReducer

Managing complex state logic with reducers

🔄 What is useReducer?

useReducer is a React Hook for managing complex state logic. It's an alternative to useState when you have multiple state values or complex state transitions that depend on previous state.


import { useReducer } from 'react';

function Counter() {
  const [count, dispatch] = useReducer(reducer, 0);
  
  return <button onClick={() => dispatch({ type: 'increment' })}>
    Count: {count}
  </button>;
}
                                    

useReducer Concepts

📦

State

Current state value managed by reducer

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

Action

Object describing what happened

dispatch({ type: 'increment', payload: 5 });
🔧

Reducer

Function that updates state based on action

function reducer(state, action) {
  return newState;
}
📤

Dispatch

Function to trigger state updates

dispatch({ type: 'reset' });

🔹 Basic Counter Example

A simple counter demonstrates useReducer fundamentals. The reducer function handles different action types to update the count state accordingly.

import { useReducer } from 'react';

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

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

Output:

Count: 0

🔹 Complex State Example

useReducer shines when managing objects with multiple properties. This todo list example shows how to handle complex state updates with different actions.

import { useReducer, useState } from 'react';

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'delete':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [text, setText] = useState('');

  const handleAdd = () => {
    if (text.trim()) {
      dispatch({ type: 'add', text });
      setText('');
    }
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleAdd}>Add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => dispatch({ type: 'toggle', id: todo.id })}
            />
            <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'delete', id: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Output:

  • Sample todo item

🔹 When to Use useReducer

Choose useReducer over useState in these scenarios:

✅ Use useReducer when:

  • Complex state logic: Multiple sub-values or state transitions
  • Next state depends on previous: State updates based on current state
  • Multiple actions: Many different ways to update state
  • Testing: Reducer functions are easy to test independently

❌ Use useState when:

  • Simple state: Single primitive values (string, number, boolean)
  • Independent updates: State changes don't depend on previous state
  • Few updates: Only one or two ways to change state

🔹 useReducer with Payload

Pass additional data with actions using the payload property. This allows you to send values needed for state updates.

function reducer(state, action) {
  switch (action.type) {
    case 'set_name':
      return { ...state, name: action.payload };
    case 'set_age':
      return { ...state, age: action.payload };
    case 'reset':
      return { name: '', age: 0 };
    default:
      return state;
  }
}

function UserForm() {
  const [user, dispatch] = useReducer(reducer, { name: '', age: 0 });

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => dispatch({ type: 'set_name', payload: e.target.value })}
        placeholder="Name"
      />
      <input
        type="number"
        value={user.age}
        onChange={(e) => dispatch({ type: 'set_age', payload: Number(e.target.value) })}
        placeholder="Age"
      />
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <p>Name: {user.name}, Age: {user.age}</p>
    </div>
  );
}

🧠 Test Your Knowledge

What does the dispatch function do in useReducer?