🧭 Introduction
As your React applications grow, state management often becomes complex:
- Multiple related state values
- Conditional updates
- Repeated state logic
- Difficult-to-read
useStatechains
This is where useReducer shines.
Many developers think useReducer is “just Redux lite” — but that’s not the right mental model.
In this lesson, you’ll learn:
- Why
useReducerexists - When to use it instead of
useState - How it improves readability, predictability, and scalability
🎯 What You’ll Learn in This Lesson
By the end of this lesson, you will understand:
- The problem with complex
useStatelogic - What
useReduceractually does - Reducer pattern fundamentals
- Action-based state updates
- Real-world
useReducerexample - Best practices and common mistakes
❓ Why useState Breaks Down for Complex Logic
useState works great for simple, independent state.
Simple useState (Perfect)
const [count, setCount] = useState(0);
But look at this 👇
❌ Complex useState Example
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
Now add conditions:
- Reset error on success
- Prevent update while loading
- Handle multiple actions
👉 Logic becomes scattered and fragile.
🧠 What is useReducer? (Clear Definition)
useReducer is:
A hook that manages state using a reducer function and actions.
Instead of saying:
“Set this value”
You say:
“This action happened — update state accordingly”
This leads to:
✅ Predictable updates
✅ Centralized logic
✅ Easier debugging
🧩 useReducer Syntax
const [state, dispatch] = useReducer(reducer, initialState);
Where:
state→ current statedispatch→ sends actionsreducer→ decides how state changesinitialState→ starting state
🔁 Reducer Function Explained
A reducer is a pure function:
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
Key Rules of Reducers
✅ Must be pure
✅ No side effects
✅ Must return new state
❌ Must not mutate state
🧪 Simple useReducer Example
import { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<h2>{state.count}</h2>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
</>
);
}
👉 State updates are explicit and predictable.
🧠 Why useReducer Scales Better
Compare mental models:
useState
setCount(count + 1);
❓ Why did state change?
❓ What triggered it?
useReducer
dispatch({ type: "INCREMENT" });
✅ Clear intent
✅ Action-driven updates
This is extremely useful for:
- Forms
- Wizards
- Complex UI flows
- Async state handling
🔍 Real-World Example — Form State
Initial State
const initialState = {
value: "",
error: null,
touched: false
};
Reducer
function reducer(state, action) {
switch (action.type) {
case "CHANGE":
return {
...state,
value: action.payload,
error: null
};
case "BLUR":
return {
...state,
touched: true
};
case "ERROR":
return {
...state,
error: action.payload
};
default:
return state;
}
}
Component
const [state, dispatch] = useReducer(reducer, initialState);
<input
value={state.value}
onChange={(e) =>
dispatch({ type: "CHANGE", payload: e.target.value })
}
onBlur={() => dispatch({ type: "BLUR" })}
/>
👉 All form logic lives in one place.
⚠ Common Mistakes with useReducer
❌ Mistake 1: Using useReducer for Everything
If state is simple → useState is better.
❌ Mistake 2: Putting Side Effects in Reducer
// ❌ WRONG
case "FETCH":
fetch("/api");
Reducers must stay pure.
❌ Mistake 3: Overcomplicated Action Types
Keep actions meaningful and readable.
🧠 useReducer vs useState (Quick Comparison)
| Scenario | Recommended |
|---|---|
| Simple toggle | useState |
| Independent values | useState |
| Related state logic | useReducer |
| Complex transitions | useReducer |
| Predictable updates | useReducer |
🔗 useReducer + useContext (Preview)
In advanced apps:
useReducerhandles logicuseContextshares state
👉 This pattern scales beautifully
(We’ll cover this later.)
🎯 Best Practices
✅ Keep reducer logic centralized
✅ Use clear action names
✅ Avoid side effects in reducer
✅ Prefer readability over cleverness
❓ FAQs — useReducer Hook
🔹 Is useReducer better than Redux?
No. It solves local component state, not global state.
🔹 Can useReducer replace useState?
Only when logic becomes complex.
🔹 Does useReducer improve performance?
Not automatically — it improves clarity and predictability.
🔹 Is useReducer mandatory for advanced apps?
No, but knowing when to use it is mandatory.
🧠 Quick Recap
✔ useReducer manages complex state
✔ State updates happen via actions
✔ Reducers are pure functions
✔ Logic becomes predictable
✔ Excellent for forms & workflows
🎉 Conclusion
useReducer is not about writing more code —
it’s about writing better, clearer, and scalable code.
Once you understand it:
- State logic stops being messy
- Bugs reduce dramatically
- Code becomes self-documenting
This lesson marks your entry into truly advanced state logic ⚛️🧠
👉 Next Lesson
Lesson 5 — useRef Hook (Beyond DOM Access)