Skillversitas

useReducer Hook

What Is useReducer?

useReducer is an alternative to useState for more complex state management. It uses the reducer pattern inspired by Redux.

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

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

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

Dispatching Actions

dispatch sends an action to the reducer to update state.

// An action is an object with a type property
dispatch({ type: 'add_todo', payload: { text: 'Learn React' } });
dispatch({ type: 'toggle_todo', payload: { id: 1 } });
dispatch({ type: 'remove_todo', payload: { id: 1 } });

The Reducer Function

The reducer receives the current state and an action, and returns a new state.

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

Comparing useReducer and useState

Aspect useState useReducer
Complexity Simple state Complex state
Update logic In the component Centralized in the reducer
Testing Hard Easy (pure function)
Related state Independent Can be related

Example: Todo List

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

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!input.trim()) return;
    dispatch({ type: 'add', payload: input.trim() });
    setInput('');
  };

  return (
    <div>
      <h2>Todo List</h2>
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="Add a todo..."
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              onClick={() => dispatch({ type: 'toggle', payload: todo.id })}
              style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'remove', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>

      <p>Remaining: {todos.filter(t => !t.done).length} todos</p>
    </div>
  );
}

Lazy Initialization

function init(initialCount) {
  return { count: initialCount };
}

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  // ...
}

useReducer is ideal for state with complex update logic. Next we will study custom hooks to reuse logic across components.