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
| Step | What happens |
|---|---|
| 1 | The user clicks "Delete" |
| 2 | The component calls dispatch with a deleted action |
| 3 | React calls todosReducer(todos, action) |
| 4 | The reducer returns a new array |
| 5 | React 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
useState | useReducer |
|---|---|
| One or two simple values | An object or array with many operations |
| Update logic fits in one line | The logic is worth keeping in one place |
| A counter, a flag, a field's text | A 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.