useReducer Hook
Learn when to use React useReducer hook instead of useState for better state management in complex components.
React useReducer
hook is a powerful alternative to the useState
hook for managing complex states efficiently. If your state logic involves multiple conditions or dependencies, useReducer
provides a structured way to handle it.
What is useReducer?
useReducer
is a React Hook that helps manage state using a functional approach.
Instead of directly updating the state (like useState
), it uses a reducer function that receives an action and decides how to update the state. It follows a dispatch-action pattern, similar to Redux.
When to use useReducer over useState?
- If state logic is simple,
useState
is enough. - If state updates depend on previous values,
useReducer
is better. - If multiple state updates happen together,
useReducer
keeps things structured.
Basic Syntax of useReducer
Before using useReducer
, you need to import it from React:
import { useReducer } from "react";
Syntax:
const [state, dispatch] = useReducer(reducerFunction, initialState);
Here,
reducerFunction(state, action)
: A function that handles the logic of updating the state.initialState
: Initial value of the state.dispatch(action)
: Triggers state updates based on an action.
useState vs useReducer Comparison (Counter App)
To understand useReducer
better, let’s first build a simple counter app using useState
.
Using useState:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default Counter;
How does it work?
useState
is used to store the count value.- The buttons update the state directly using
setCount( )
.
Now, let’s see how you can achieve the same using useReducer
.
Using useReducer:
import { 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;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>Increment</button>
<button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
export default Counter;
How does it work?
- The reducer function determines how the state changes based on the action type.
- Instead of
setCount( )
, you calldispatch( )
with an action.
Complex State Management (Todo App)
If you build a Todo App in which multiple states (add, delete, toggle complete) are updated, then the useReducer
hook is better than useState
.
Todo App Using useState:
import { useState } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const addTodo = () => {
setTodos([...todos, { text: input, completed: false }]);
setInput("");
};
const toggleTodo = (index) => {
setTodos(
todos.map((todo, i) =>
i === index ? { ...todo, completed: !todo.completed } : todo
)
);
};
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index));
};
return (
<div>
<h2>Todo App using useState</h2>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map((todo, index) => (
<li key={index} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
<button onClick={() => toggleTodo(index)}>Toggle</button>
<button onClick={() => removeTodo(index)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
How does it work?
State Management:
- todos: An array storing todo items ({ text, completed }).
- input: Stores the current value of the input field.
Adding a Todo:
- Creates a new todo object with
{ text: input, completed: false }
. - Updates the state using
setTodos([…todos, newTodo])
. - Clears the input field after adding a todo.
Toggling Todo Completion:
- Uses
.map()
to iterate over todos. - Finds the todo by index and updates its completed status (true ↔ false).
- Returns a new array with updated values (immutability).
Removing a Todo:
- Uses
.filter()
to create a new array excluding the selected todo. - Updates the todos state with the filtered list.
Todo App using useReducer:
import { useReducer, useState } from "react";
function todoReducer(state, action) {
switch (action.type) {
case "add":
return [...state, { text: action.payload, completed: false }];
case "toggle":
return state.map((todo, index) =>
index === action.index ? { ...todo, completed: !todo.completed } : todo
);
case "remove":
return state.filter((_, index) => index !== action.index);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [input, setInput] = useState("");
return (
<div>
<h2>Todo App using useReducer</h2>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={() => dispatch({ type: "add", payload: input })}>Add Todo</button>
<ul>
{todos.map((todo, index) => (
<li key={index} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
<button onClick={() => dispatch({ type: "toggle", index })}>Toggle</button>
<button onClick={() => dispatch({ type: "remove", index })}>Remove</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
How does it work?
Reducer Function (todoReducer):
- Handles state updates based on action types.
- Uses a switch statement to determine how the state changes.
Action Types & Their Effects:
- “add”: Adds a new todo
({ text: action.payload, completed: false })
. - “toggle”: Finds the todo by index and toggles its completed state.
- “remove”: Filters out the todo at the given index.
Using useReducer Hook:
const [todos, dispatch] = useReducer(todoReducer, []);
todos
stores the list of todo items.dispatch(action)
triggers state updates viatodoReducer
.
Handling Input & Actions:
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={() => dispatch({ type: "add", payload: input })}>Add Todo</button>
- Input is managed using
useState
. dispatch({ type: “add”, payload: input })
adds a new todo.
Rendering Todos:
{todos.map((todo, index) => (
<li key={index} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
{todo.text}
<button onClick={() => dispatch({ type: "toggle", index })}>Toggle</button>
<button onClick={() => dispatch({ type: "remove", index })}>Remove</button>
</li>
))}
- Maps over todos to display each task.
- “Toggle” button updates the completion status.
- “Remove” button deletes the todo.
You can see, with multiple state updates (add, remove, toggle todos), how useReducer
simplifies the logic. Instead of multiple setState
calls, a single dispatch(action) updates the state predictably. This makes it easier to scale and debug compared to multiple useState
calls.
How useReducer help?
- Centralized logic for handling state.
- Avoids unnecessary re-renders caused by multiple
useState
updates. - Scales well as your application grows.
useReducer vs useState: When to use?
Feature | useState | useReducer |
---|---|---|
State Complexity | Best for simple state | Best for complex state |
Multiple State Updates | This can create confusion in multiple state update | Actions and reducer make it easy |
Performance | Fast for small state | Optimized for large & complex state |
Use Case | Basic counters, form inputs, UI state | Forms, Todos, complex logic, state dependency |
Best Practices & Common Mistakes with useReducer
- Use the
useReducer
if multiple state changes are dependent on each other. - Define the type for every action to make debugging easy.
- Use
useReducer
only when the state is complex; don’t take this as a replacement foruseState
in every case.
Mistake: Using useReducer for every small state
const [state, dispatch] = useReducer(reducer, false);
dispatch({ type: "toggle" });
This is a simple toggle, so useState
is a better option for this.
Mistake: Not Using Action Payloads for Dynamic Updates
dispatch({ type: "addTodo" }); // No data passed!
Solution: Pass a payload with the necessary data. This makes reducers more flexible and reusable.
dispatch({ type: "addTodo", payload: "Learn useReducer" });