From Simple State to Scalable Logic
Welcome back to another episode of “Let’s Master React Hooks Together”.
React always feels easy at the beginning.
You learn components.
You learn props.
You learn useState.
Everything feels smooth.
But then real applications arrive.
And suddenly state logic starts growing fast:
- multiple form fields
- loading states
- error handling
- API responses
- step-based flows
- conditional updates
And your component slowly becomes harder to manage.
This is where most developers get stuck.
Because useState works perfectly… until it doesn’t.
That’s where useReducer enters.
Not as a replacement.
But as a way to bring structure when state starts becoming complex.
In This Episode, You’ll Learn
- What useReducer actually is
- How state flows through it
- How actions connect everything
- How dispatch triggers updates
- Real-world patterns
- Multi-step form logic
- When to use it and when not to
And most importantly…
You’ll see how scattered state turns into a clean, predictable flow.
What is useReducer in React?
At its core, useReducer follows one simple idea:
Instead of changing state directly, you describe what happened, and React figures out how the state should change.
That movement always follows the same path:
Action → Reducer → New State
Each part has a role:
- Action carries the event
- Reducer decides the change
- State stores the result
Why This Pattern Exists
Think about a login screen.
You are dealing with multiple values:
- password
- loading
- error
- success
If each one is handled separately, updates happen in different places, and logic becomes scattered.
Over time, it becomes harder to track how everything connects.
The idea of a reducer is to bring all these changes into one controlled place where every update follows the same path.
The Foundation
Everything starts with this line:
const [state, dispatch] = useReducer(reducer, initialState)
This creates a connection between three things:
- state → holds current values
- dispatch → sends a signal
- reducer → decides what happens next
Nothing changes yet — this only sets up the system.
State Begins the Journey
Now imagine state starting like this:
{
count: 0
}
This becomes the single source of truth.
Every change in the future will create a new version of this structure instead of modifying it directly.
That’s why React can reliably update the UI — because every update is a fresh object.
Dispatch Starts Everything
Now something happens in the UI.
Instead of changing state directly, a message is sent:
dispatch({ type: "increment" })
At this point, nothing has changed yet.
This is just an instruction saying:
“something happened”.
That instruction now moves to the central decision point.
The Decision Point
Everything reaches this 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
}
}
Now break this flow mentally:
- state gives current value
- action tells what happened
- switch chooses the path
Each path creates a new version of state.
Nothing is modified directly — everything is replaced with a new result.
Connecting It All in a Component
Now everything comes together:
const initialState = {
count: 0
}
This sets the starting point.
Now the reducer defines how this value will change over time:
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 }
case "decrement":
return { count: state.count - 1 }
default:
return state
}
}
Now the component connects everything:
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<h1>{state.count}</h1>
<button onClick={() =>
dispatch({ type: "increment" })
}>
+
</button>
<button onClick={() =>
dispatch({ type: "decrement" })
}>
-
</button>
</div>
)
}
Now follow the full loop:
User clicks button → dispatch sends action → reducer receives it → state updates → UI re-renders
Everything stays inside one predictable cycle.
Actions Carry Intent
An action is always simple:
dispatch({
type: "increment"
})
It does not change anything directly.
It only describes what happened so the reducer can decide what to do.
When Data Is Needed
Sometimes actions carry extra information:
dispatch({
type: "ADD_USER",
payload: {
name: "John"
}
})
Now the reducer uses that data:
case "ADD_USER":
return {
users: [...state.users, action.payload]
}
Now the same pattern continues:
event → action → reducer → new state
Real Flow — Multi-Step Form
Now this pattern expands into real applications.
A form usually has multiple connected parts:
- step tracking
- form data
- validation errors
So everything is grouped together:
const initialState = {
step: 1,
formData: {
name: "",
email: ""
},
errors: {}
}
Now every change must respect this structure.
Updating Form Fields
Instead of writing separate logic for each input, everything flows through one pattern:
function reducer(state, action) {
switch (action.type) {
case "UPDATE_FIELD":
return {
...state,
formData: {
...state.formData,
[action.field]: action.value
}
}
case "NEXT_STEP":
return {
...state,
step: state.step + 1
}
default:
return state
}
}
Now focus on the flow:
- existing state is copied
- only one field inside formData changes
- everything else stays intact
That’s how structure is preserved.
Input Connection
Each input follows the same path:
onChange={(e) =>
dispatch({
type: "UPDATE_FIELD",
field: "name",
value: e.target.value
})
}
Now every keystroke follows the same cycle:
user input → dispatch → reducer → state update → UI refresh
No duplication. No scattered logic.
When It Makes Sense
This approach becomes useful when:
- state starts interacting with other state
- updates depend on previous values
- logic becomes harder to manage
- component starts growing fast
When It Doesn’t
If state is simple and isolated:
const [open, setOpen] = useState(false)
there is no need for extra structure.
The Core Idea
Simple state changes are independent.
Reducer-based state changes are structured flows.
That difference becomes important as applications scale.
Why Developers Prefer It
Because it naturally brings:
- predictable updates
- centralized control
- scalable structure
- clean separation of logic
- easier debugging
Redux Connection
This same pattern evolves into Redux:
actions → reducers → dispatch → store updates
So understanding this now builds a strong foundation for larger systems later.
Final Thoughts
At this stage, React is no longer just about writing UI.
It becomes about designing how data moves.
Because the real difference between simple apps and scalable apps is not syntax…
It’s structure.
And useReducer introduces that structure.
Instead of asking:
“How do I change this value?”
You start asking:
“How should this value change over time?”
That shift is what separates writing code from designing applications.
What to Remember
When state starts feeling scattered:
- Bring it into one flow.
- Let actions describe events.
- Let reducers decide outcomes.
That single mindset shift is what levels up your React thinking.
Next Episode
In the next episode of “Let’s Master React Hooks Together”, we’ll explore advanced React patterns that build on this foundation and take your applications closer to production-level architecture.
Stay consistent.
Top comments (0)