Hey Folks!
Today, I just want to share what I learned in class. I hope this will be useful for beginner and intermediate-level budding developers as well.
Right now, I’m not working on large-scale projects yet. The hook I learned today is mostly used in complex applications.
When a project becomes more complex, we may end up using many useState hooks. At that point, the code can become messy and difficult to manage. That’s where this hook becomes very useful.
Here's exactly what we'll cover:
- What
useReduceractually is — in plain English - The structure — broken down piece by piece
- Why it exists and when
useStatejust isn't enough - A real use case — a shopping cart — so you feel it, not just read it
1. What Is useReducer?
useReducer is a React Hook — just like useState — but it's built for managing state that has multiple moving parts.
Think of it like this.
useState is a light switch. One thing. On or off.
useReducer is a remote control. Many buttons. Each button does something different. But there's only one remote.
The idea is simple: instead of writing five different setState calls scattered across your component, you write one function — the reducer — that handles all the state changes in one place.
One function. One truth. All changes flow through it.
2. The Structure — What Are All These Pieces?
This is where most tutorials lose you. Let's slow down and meet each piece.
const [state, dispatch] = useReducer(reducer, initialState);
What just happened here is — we're setting up our "state remote control."
-
state— the current state value (likestateinuseState) -
dispatch— the function you call to trigger a change (replacessetState) -
reducer— your function that decides how state changes -
initialState— what state looks like at the very beginning
Now let's look at the reducer function itself.
// This function decides how state should change based on what happened
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; // if nothing matches, don't change anything
}
}
What just happened here is — the reducer is like a security guard at the gate. You tell him what happened (the action), and he decides what to do (the new state).
The action is just an object with a type field — like a label. "INCREMENT" means "I want to go up." "DECREMENT" means "go down." You name them. You decide.
And the dispatch function? That's how you talk to the guard.
// Calling dispatch is like pressing a button on the remote
dispatch({ type: "INCREMENT" });
dispatch({ type: "RESET" });
You don't set the state. You describe what happened — and the reducer figures out the rest.
3. Why Does useReducer Even Exist?
useState is great. Honestly, for simple things — a modal open/close, a form input value — use it. No shame.
But imagine you're building a form with 6 fields.
// This starts to feel like juggling
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [step, setStep] = useState(1);
const [submitted, setSubmitted] = useState(false);
Now imagine that pressing "Submit" needs to: set loading to true, clear the error, and move to step 2 — all at once.
You'd write:
setLoading(true);
setError(null);
setStep(2);
Three separate calls. Three re-renders (potentially). And if any of them depend on each other? Things get messy fast.
useReducer fixes this by bundling all of that state into one object — and all of those changes into one function.
// One dispatch. All three things happen together.
dispatch({ type: "SUBMIT_FORM" });
Clean. Predictable. And when something breaks, you know exactly where to look — the reducer.
4. A Real Use Case — Shopping Cart
Let's build something real. A simple cart that can add items, remove items, and clear everything.
This is exactly the kind of feature where useReducer shines.
import { useReducer } from "react";
// Starting point — the cart is empty
const initialState = {
items: [],
};
// The reducer — all cart logic lives here
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM":
// Add the new product to the items array
return { items: [...state.items, action.payload] };
case "REMOVE_ITEM":
// Keep everything except the item with the matching id
return {
items: state.items.filter((item) => item.id !== action.payload),
};
case "CLEAR_CART":
// Wipe everything — start fresh
return { items: [] };
default:
return state;
}
}
What just happened here is — we wrote all the cart logic in one place. Add. Remove. Clear. The reducer is the single source of truth for everything cart-related.
Now the component:
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
// These are the products available to add
const products = [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Headphones", price: 199 },
{ id: 3, name: "Mouse", price: 49 },
];
return (
<div>
<h2> Cart ({state.items.length} items)</h2>
{/* Show what's in the cart */}
{state.items.map((item, index) => (
<div key={index}>
<span>{item.name} — ₹{item.price}</span>
{/* Tell the reducer: "remove this item" */}
<button onClick={() => dispatch({ type: "REMOVE_ITEM", payload: item.id })}>
Remove
</button>
</div>
))}
<hr />
{/* Show available products */}
{products.map((product) => (
<div key={product.id}>
<span>{product.name}</span>
{/* Tell the reducer: "add this product to the cart" */}
<button onClick={() => dispatch({ type: "ADD_ITEM", payload: product })}>
Add to Cart
</button>
</div>
))}
{/* Wipe the entire cart */}
<button onClick={() => dispatch({ type: "CLEAR_CART" })}>
Clear Cart
</button>
</div>
);
}
export default ShoppingCart;
What just happened here is — the component doesn't think about how state changes. It just says what happened: "ADD_ITEM", "REMOVE_ITEM", "CLEAR_CART". The reducer handles the rest.
That's the beauty of it. Your UI describes events. Your reducer handles logic. They never mix.
5. Common Mistakes Beginners Make
Mistake 1 — Mutating state directly inside the reducer
// Wrong — never mutate state directly
case "ADD_ITEM":
state.items.push(action.payload); // this breaks React's re-render
return state;
// Right — always return a NEW object
case "ADD_ITEM":
return { items: [...state.items, action.payload] };
React needs a new reference to know that something changed. If you mutate the old state, it sees "same object" and skips the re-render.
Mistake 2 — Forgetting the default case
Always include default: return state. Without it, an unrecognized action type silently breaks your state.
Mistake 3 — Using useReducer for simple toggles
If your state is just true or false, useState is the right tool. useReducer is for complex state — multiple fields, interdependent updates, or many action types.
Top comments (0)