useReducer Hook — Managing Complex State in React
React's useReducer hook is designed for cases where state is complex, has multiple subdomains or operations. It resembles the Redux concept, but is a simpler version without additional libraries.
If useState is sufficient for simple values or small objects, useReducer is suitable when we have multiple actions that change the same state in different ways.
🔹 Syntax
const [state, dispatch] = useReducer(reducer, initialState);
- reducer — function that determines how to change state based on action.
- initialState — initial value of the state.
- dispatch — function we call to make changes.
📍 Example 1 — Counter App with useReducer
Let's start with the simplest example: a counter.
import React, { useReducer } from 'react';
// reducer function
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<h2>Count: {state.count}</h2>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
In this example, the reducer receives two values:
state and action.
Action typically has type and sometimes payload.
🧠 Why the Reducer Pattern is Clearer
The power of useReducer lies in keeping all state change logic in one place — the reducer. This makes the project predictable and easy to debug.
For example, imagine having several buttons that need to change state in different ways. With useState, that logic would be scattered across different functions. With useReducer — everything is centralized in one reducer.
📍 Example 2 — ToDo List with Reducer Pattern
Here's a professional example: task management via reducer.
import React, { useReducer, useState } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.payload, done: false }];
case 'toggle':
return state.map(task =>
task.id === action.payload ? { ...task, done: !task.done } : task
);
case 'delete':
return state.filter(task => task.id !== action.payload);
default:
return state;
}
}
export default function TodoApp() {
const [tasks, dispatch] = useReducer(reducer, []);
const [text, setText] = useState('');
function handleAdd() {
if (text.trim()) {
dispatch({ type: 'add', payload: text });
setText('');
}
}
return (
<div>
<h2>Task List</h2>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add a task"
/>
<button onClick={handleAdd}>Add</button>
<ul>
{tasks.map(task => (
<li key={task.id}>
<span
style={{ textDecoration: task.done ? 'line-through' : 'none', cursor: 'pointer' }}
onClick={() => dispatch({ type: 'toggle', payload: task.id })}
>
{task.text}
</span>
<button onClick={() => dispatch({ type: 'delete', payload: task.id })}>✖</button>
</li>
))}
</ul>
</div>
);
}
This example demonstrates the full power of the reducer pattern: all actions (add, delete, toggle) are performed within one clearly managed reducer.
⚡ Tips for Professionals
- Reducer should be a pure function — without side effects (fetch, log, localStorage, etc.).
- If state is a complex object, use immutable update pattern.
- Reducer should always return a new state object.
- Build actions with clear types and optionally payload.
💡 useReducer vs useState
| useState | useReducer |
|---|---|
| Simple state (e.g., input value, counter) | Complex state (e.g., form data, lists, nested objects) |
| Changes are in different functions | Changes are within one reducer |
| Fast and simple | Structured and predictable |
🧩 Complex Example — Nested State Reducer
If you have nested object state, the reducer can update only the necessary subfields in an immutable way.
function reducer(state, action) {
switch (action.type) {
case 'updateProfile':
return {
...state,
profile: { ...state.profile, name: action.payload }
};
case 'toggleNotifications':
return {
...state,
settings: { ...state.settings, notifications: !state.settings.notifications }
};
default:
return state;
}
}
const initialState = {
profile: { name: 'Aram', email: 'aram@example.com' },
settings: { theme: 'light', notifications: true }
};
🎯 Exercise (Try it yourself)
Write a reducer that manages the following:
- Can add products to "cart".
- Can remove products from cart.
- Can calculate total price.
Use immutable updates and action.payload system.
📘 Summary
- useReducer — React hook for managing complex state.
- Reducer receives (state, action) and returns new state.
- Reducer should be a pure function — without side effects.
- Use dispatch actions to centralize changes.
- Reducer pattern makes state management predictable and clear.