Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

useReducer

When state can change in many ways — add, delete, toggle, clear — the handler functions spread all over the component. The useReducer hook collects that logic in one function, the reducer. The component only reports what happened by dispatching an action: an object like { type: 'added', text: '...' }.

The reducer

A reducer is a plain function with no React in it: it takes the current state and an action and returns the new state. Create src/todosReducer.js:

// src/todosReducer.js
export function todosReducer(todos, action) {
  switch (action.type) {
    case 'added':
      return [...todos, { id: crypto.randomUUID(), text: action.text, done: false }];
    case 'toggled':
      return todos.map((todo) =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo,
      );
    case 'deleted':
      return todos.filter((todo) => todo.id !== action.id);
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

The rules are the same as with useState: do not change the array in place, return a new one.

Using it in a component

useReducer takes the reducer and the initial state and returns the state and a dispatch function for sending actions:

// src/App.jsx
import { useReducer } from 'react';
import { todosReducer } from './todosReducer.js';
import AddTodoForm from './components/AddTodoForm.jsx';
import TodoList from './components/TodoList.jsx';
 
const initialTodos = [{ id: 1, text: 'Buy milk', done: false }];
 
export default function App() {
  const [todos, dispatch] = useReducer(todosReducer, initialTodos);
 
  return (
    <>
      <AddTodoForm onAdd={(text) => dispatch({ type: 'added', text })} />
      <TodoList
        todos={todos}
        onToggle={(id) => dispatch({ type: 'toggled', id })}
        onDelete={(id) => dispatch({ type: 'deleted', id })}
      />
    </>
  );
}

The page works exactly as with useState: tasks are added, toggled and deleted. But all the logic is now in one file.

How it works

StepWhat happens
1The user clicks "Delete"
2The component calls dispatch with a deleted action
3React calls todosReducer(todos, action)
4The reducer returns a new array
5React re-renders the component with the new state

A reducer is easy to check

A reducer does not depend on React, so it can be called like any function — for example in a test: todosReducer([], { type: 'added', text: 'Test' }) returns an array with one task.

useState or useReducer

useStateuseReducer
One or two simple valuesAn object or array with many operations
Update logic fits in one lineThe logic is worth keeping in one place
A counter, a flag, a field's textA cart, a task list, a multi-step form

Common mistake

A reducer must be pure: it only computes the new state from the old one and the action. Server requests, side-effect console.log calls or changing the given array inside it lead to hard-to-find bugs:

// src/todosReducer.js
// Wrong: the array is changed in place.
case 'added':
  todos.push({ id: crypto.randomUUID(), text: action.text, done: false });
  return todos;

Return a new array, and make requests in event handlers or effects.

Thanks for reading!

Donate to the site — we work hard for you.