useReducer Hook
useReducer Hook — complex state management 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.
const [state, dispatch] = useReducer(reducer, initialState);
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.
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.
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.
| 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 |
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 }
};
Write a reducer that manages the following:
Use immutable updates and action.payload system.