Welcome back to the React Mastery Series!
In the previous article, we explored the Context API and learned how to eliminate prop drilling by sharing data across multiple components.
However, as applications become more complex, managing state using multiple useState() Hooks can quickly become difficult.
Imagine building:
- An online banking dashboard
- A shopping cart
- A task management application
- A multi-step registration form
Each of these applications may involve dozens of related state updates.
React provides another Hook specifically designed for handling complex state logic useReducer
Why Do We Need useReducer?
Let's first understand the problem.
Suppose you're building a shopping cart.
Using useState, you might write:
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [discount, setDiscount] = useState(0);
const [tax, setTax] = useState(0);
const [shipping, setShipping] = useState(100);
Now imagine every action updates multiple states.
Adding a product:
- Updates items
- Recalculates total
- Recalculates tax
- Applies discounts
- Updates shipping
Managing all this logic separately becomes difficult.
What is useReducer?
useReducer is a Hook used to manage complex state transitions.
Instead of updating state directly, we:
- Dispatch an action
- Let a reducer decide how state should change
Think of it like this:
User Action
|
↓
Dispatch Action
|
↓
Reducer Function
|
↓
New State
|
↓
Component Re-renders
The reducer becomes the central place where all state updates happen.
Basic Syntax
const [state, dispatch] = useReducer(reducer, initialState);
Parameters:
-
reducer→ Function that updates the state -
initialState→ Starting value
Returns:
statedispatch
Understanding the Reducer Function
The reducer receives:
(state, action)
Example:
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return {
count: state.count + 1
};
case "decrement":
return {
count: state.count - 1
};
default:
return state;
}
}
A reducer always:
- Receives current state
- Receives an action
- Returns new state
It should never mutate the existing state.
Initial State
const initialState = {
count: 0
};
React uses this value during the first render.
Creating the Reducer
const [state, dispatch] = useReducer( counterReducer, initialState);
Now we can access:
state.count
Dispatching Actions
Instead of:
setCount(count + 1);
we dispatch an action.
Example:
dispatch({type: "increment"});
Flow:
Button Click
|
↓
dispatch()
|
↓
Reducer Executes
|
↓
Returns New State
|
↓
React Re-renders
Complete Counter 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 (
<div>
<h2>{state.count}</h2>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
}
Understanding Actions
Actions are plain JavaScript objects.
Example:
{
type: "increment"
}
Actions can also carry additional data.
Example:
{
type: "deposit",
amount: 5000
}
The reducer decides what to do with that information.
Passing Data with Payload
Example:
dispatch({ type: "deposit", amount: 5000});
Reducer:
case "deposit":
return {balance: state.balance + action.amount};
This pattern is widely used in enterprise applications.
Real-World Example: Bank Account
Initial state:
const initialState = {
balance: 10000
};
Reducer:
function reducer(state, action) {
switch (action.type) {
case "deposit":
return {balance: state.balance + action.amount};
case "withdraw":
return {balance: state.balance - action.amount};
default:
return state;
}
}
Flow:
Deposit Button
|
↓
Dispatch Action
|
↓
Reducer Updates Balance
|
↓
UI Refreshes
Why Not Just Use useState?
Consider a login form.
Using multiple states:
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [user, setUser] = useState(null);
As the application grows, related state becomes harder to manage.
With useReducer:
const initialState = {
email: "",
password: "",
loading: false,
error: "",
user: null
};
Everything is managed in one predictable place.
Combining Context with useReducer
One of the most common enterprise patterns is:
Context API
|
↓
useReducer
|
↓
Global State
Example:
<AuthContext.Provider
value={{
state,
dispatch,
}}
>
<App />
</AuthContext.Provider>;
Now every component can dispatch actions.
Action Flow in Enterprise Applications
User Clicks Login
|
↓
Dispatch LOGIN_REQUEST
|
↓
Reducer Updates Loading State
|
↓
API Call
|
↓
Dispatch LOGIN_SUCCESS
|
↓
Reducer Stores User
|
↓
Dashboard Updates
This architecture is very similar to Redux.
Common Action Types
Examples:
LOGIN_REQUEST
LOGIN_SUCCESS
LOGIN_FAILURE
FETCH_USERS
ADD_PRODUCT
DELETE_PRODUCT
UPDATE_PROFILE
LOGOUT
Action names should clearly describe what happened.
Common Mistakes
1. Mutating State
Incorrect:
state.count++;
return state;
Correct:
return {
count: state.count + 1
};
Always return a new object.
2. Too Much Logic Inside Components
Avoid:
if(user.role === "ADMIN"){
// update many states
}
Move business logic into the reducer whenever possible.
3. Missing Default Case
Always include:
default:
return state;
This ensures unknown actions don't break your application.
useState vs useReducer
| useState | useReducer |
|---|---|
| Simple state | Complex state |
| Easy to learn | Better for large logic |
| Direct updates | Action-based updates |
| Few state variables | Multiple related state values |
| Best for local UI | Best for predictable state transitions |
Enterprise Example: Retail Banking Dashboard
Imagine a customer dashboard.
Available actions:
Load Accounts
Load Transactions
Transfer Money
Update Profile
Logout
Flow:
User Action
|
↓
Dispatch Action
|
↓
Reducer
|
↓
Update Global State
|
↓
React Re-renders UI
Every state change follows the same predictable pattern.
Best Practices
- Use
useStatefor simple state. - Use
useReducerfor complex or related state. - Keep reducers pure.
- Never mutate state directly.
- Use meaningful action names.
- Separate reducer logic from UI components.
- Combine
useReducerwith Context for scalable applications.
Key Takeaways
Today, we learned:
✅ useReducer manages complex state transitions.
✅ Reducers receive the current state and an action.
✅ Actions describe what happened, while reducers decide how state changes.
✅ dispatch() sends actions to the reducer.
✅ useReducer works exceptionally well with the Context API.
✅ The architecture is similar to Redux, making it easier to learn Redux later.
Coming Next 🚀
In Day 18, we will explore:
Custom Hooks in React – Reusing Logic Like a Pro
We will learn:
- What Custom Hooks are
- Why they are useful
- Creating reusable Hooks
- Sharing logic across components
- Real-world examples
- Best practices and common mistakes
Custom Hooks are one of the most powerful features of React and are widely used in enterprise applications to keep code clean, reusable, and maintainable.
Happy Coding! 🚀
Top comments (0)