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.
In this post, you’ll learn about the useReducer
hook, including its syntax, how it works, and the difference between useState
and useReducer
.
Before we get started, don’t forget to subscribe to my newsletter!
Get the latest tips, tools, and resources to level up your web development skills delivered straight to your inbox. Subscribe here!
Now, let’s jump right into 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" });
🎯Wrapping Up
That’s all for today!
For paid collaboration connect with me at : connect@shefali.dev
I hope this post helps you.
If you found this post helpful, here’s how you can support my work:
☕ Buy me a coffee – Every little contribution keeps me motivated!
📩 Subscribe to my newsletter – Get the latest tech tips, tools & resources.
𝕏 Follow me on X (Twitter) – I share daily web development tips & insights.
Keep coding & happy learning!
Top comments (0)