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.