Most developers think they understand reduce().
They don't.
And to be fair, I didn't either for a long time.
Like many JavaScript developers, I learned reduce() through examples like this:
const total = [1, 2, 3, 4]
.reduce((sum, n) => sum + n, 0)
console.log(total)
// 10
Then I saw examples for:
- grouping arrays
- transforming arrays
- creating lookup tables
- counting occurrences
Eventually I came to the conclusion that:
Reduce is the Swiss Army Knife of array operations.
And while that statement is technically true, it completely misses the point.
Because the biggest misunderstanding about reduce() is this:
reduce()has almost nothing to do with arrays.
Arrays are merely where most developers first encounter the idea.
The real idea behind reduce() is much deeper.
Once you understand it, you start seeing the exact same pattern everywhere:
- Redux
- React's
useReducer - RxJS
scan - Event Sourcing
- CQRS
- State Machines
- Financial Ledgers
- Audit Logs
- Domain Events
The same abstraction keeps appearing.
And it appears because reduce() is not fundamentally about arrays.
It is about state transformation.
The Misleading Name
The name itself causes confusion.
When developers hear "reduce", they naturally think:
Many things
↓
Reduced into one thing
For example:
const total = [10, 20, 30]
.reduce((sum, n) => sum + n, 0)
or
const longestWord = words.reduce(...)
or
const max = numbers.reduce(...)
All of these examples reinforce the idea that reduce is about collapsing a collection into a single value.
That is not wrong.
But it is incomplete.
Let's look at a different example.
This Doesn't Look Like Reduction
const usersById = users.reduce(
(state, user) => {
state[user.id] = user
return state
},
{}
)
What exactly is being reduced here?
Nothing is being summed.
Nothing is being minimized.
Nothing is being collapsed.
Instead, something else is happening.
We are evolving state.
Each user changes the current state.
Current State
+
User
=
Next State
That formula is the real abstraction.
And once you see it, everything changes.
Reduce Is A State Evolution Function
Forget arrays for a moment.
Let's define a reducer.
A reducer takes:
Current State
+
Input
=
Next State
In code:
(state, input) => nextState
That's it.
That is the entire idea.
The reducer itself does not care whether:
- input came from an array
- input came from a stream
- input came from a websocket
- input came from a user action
- input came from a database event
The reducer only knows:
State + Input = New State
Everything else is implementation detail.
Redux Was Telling Us This All Along
Redux literally puts the word reducer in its API.
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return state + 1
case "DECREMENT":
return state - 1
default:
return state
}
}
Now let's process some actions.
const actions = [
{ type: "INCREMENT" },
{ type: "INCREMENT" },
{ type: "DECREMENT" }
]
const result = actions.reduce(
reducer,
0
)
console.log(result)
// 1
Look closely.
Redux's core architecture is simply:
Current State
+
Action
=
Next State
Which is exactly:
(state, action) => nextState
Which is exactly a reducer.
Redux isn't inspired by reduce.
Redux is reduce.
React's useReducer Doesn't Even Hide It
React developers use this every day.
const [state, dispatch] =
useReducer(reducer, initialState)
The React team could have named it anything.
They could have called it:
useStateMachine
useStore
useTransition
useActionState
But they didn't.
They called it:
useReducer
Because that's exactly what it is.
React isn't introducing a new idea.
It is exposing the same abstraction.
Current State
+
Action
=
Next State
Again.
State Machines Are Just Reducers
Let's build a traffic light.
States:
RED
GREEN
YELLOW
Events:
NEXT
Reducer:
function reducer(state, event) {
switch (state) {
case "RED":
return "GREEN"
case "GREEN":
return "YELLOW"
case "YELLOW":
return "RED"
}
}
Process events:
const events = [
"NEXT",
"NEXT",
"NEXT"
]
const finalState =
events.reduce(
reducer,
"RED"
)
console.log(finalState)
// RED
Congratulations.
You just built a state machine using reduce.
Event Sourcing Is Just Reduce Over Time
This is where things get interesting.
Suppose we have a bank account.
Events:
const events = [
{
type: "DEPOSIT",
amount: 100
},
{
type: "DEPOSIT",
amount: 50
},
{
type: "WITHDRAW",
amount: 25
}
]
Reducer:
function accountReducer(
balance,
event
) {
switch (event.type) {
case "DEPOSIT":
return balance + event.amount
case "WITHDRAW":
return balance - event.amount
default:
return balance
}
}
Current balance:
const balance =
events.reduce(
accountReducer,
0
)
console.log(balance)
// 125
Most developers would call this:
A reduce operation
An Event Sourcing architect would call this:
State reconstruction
They are the same thing.
Event Sourcing is simply:
Replay events
through a reducer
to reconstruct state.
That's it.
The entire architecture is built on the same abstraction.
CQRS Is The Same Story
CQRS often sounds intimidating.
Commands.
Events.
Projections.
But underneath:
Command
↓
Event
↓
Reducer
↓
State
A projection is simply another reducer.
For example:
function ordersByCustomer(
state,
orderCreated
) {
const {
customerId
} = orderCreated
state[customerId] ??= []
state[customerId].push(
orderCreated
)
return state
}
Process events.
Build state.
Same pattern.
Again.
RxJS scan() Is Reduce For Infinite Data
This realization blew my mind the first time I encountered it.
Normal reduce:
[1, 2, 3, 4]
.reduce(...)
requires all data to exist upfront.
But streams never end.
So RxJS introduced:
scan()
Example:
interval(1000)
.pipe(
scan(
count => count + 1,
0
)
)
Output:
1
2
3
4
5
6
...
What happened?
The stream is reducing continuously.
The state evolves forever.
A better description is:
scan()is reduce for infinite sequences.
Which means:
Reduce
↓
State Evolution
↓
scan()
Same idea.
Different environment.
The Formula That Keeps Reappearing
At this point we have seen:
- Arrays
- Redux
- React
- Event Sourcing
- CQRS
- State Machines
- RxJS
All using the same abstraction.
Because underneath everything sits one formula:
Current State
+
Input
=
Next State
Or:
(state, input) => nextState
That is the reducer pattern.
Everything else is implementation detail.
Why I Actually Use Reduce Less Nowadays
This might sound strange after everything I've written.
But understanding reduce deeply made me use it less.
Not more.
Because I now understand what it is for.
Many developers discover reduce and start replacing everything with it.
array.reduce(...)
for every transformation.
I did that too.
Then I stopped.
Because for most day-to-day code:
for (const user of users) {
...
}
is clearer.
Often faster.
Usually easier to debug.
Example:
const usersById = {}
for (const user of users) {
usersById[user.id] = user
}
versus:
const usersById =
users.reduce(
(state, user) => {
state[user.id] = user
return state
},
{}
)
Both work.
The loop is arguably easier to read.
And that is okay.
The goal is not to use reduce everywhere.
The goal is to understand the abstraction behind it.
Performance Considerations
Let's address a common argument.
Many developers claim:
Reduce is slower than loops.
In many cases, they are correct.
A simple loop often wins.
Why?
Because:
Loop
↓
Less callback overhead
↓
Less abstraction
↓
Less work
There is also another common mistake.
This:
const result =
users.reduce(
(state, user) => ({
...state,
[user.id]: user
}),
{}
)
creates a new object every iteration.
That can become extremely expensive.
Compare that to:
const result =
users.reduce(
(state, user) => {
state[user.id] = user
return state
},
{}
)
The second version mutates a local accumulator and is usually much more efficient.
Functional-looking code is not automatically performant code.
Always measure.
Pros Of Thinking In Reducers
1. Predictable State Changes
Every state transition is explicit.
2. Easy To Test
state + action = nextState
Pure functions are simple to verify.
3. Event Replay
You can reconstruct state from history.
4. Time Travel Debugging
Redux DevTools exists because reducers are deterministic.
5. Scales To Complex Systems
Works for:
- Redux
- CQRS
- Event Sourcing
- Streams
- State Machines
Cons Of Overusing Reducers
1. Cognitive Overhead
Not every transformation needs a reducer.
2. Can Become Unreadable
Huge reducers quickly become difficult to maintain.
3. Loops Are Often Simpler
Sometimes the simplest solution is the best one.
4. Functional Purity Can Hurt Performance
Excessive copying can become expensive.
5. Teams Must Understand The Pattern
Otherwise reducers become magic instead of architecture.
The Real Lesson
The biggest lesson I learned about reduce was that it had very little to do with arrays.
Arrays merely introduced me to the idea.
The real idea was this:
Current State
+
Input
=
Next State
Once you understand that formula, you start seeing reduce everywhere.
In Redux.
In React.
In RxJS.
In Event Sourcing.
In CQRS.
In State Machines.
In distributed systems.
In financial ledgers.
In software architecture itself.
And ironically, once you truly understand reduce, you stop asking:
How do I update this object?
and start asking:
What transformed this state?
That shift in thinking is far more valuable than the reduce() function itself.
About The Author
Hi, I'm Amrish Khan.
I enjoy building developer tools, exploring software architecture, and writing about the deeper ideas behind everyday programming concepts.
I'm also building Aruvix — a growing ecosystem of local-first developer tools designed to process data directly in the browser without unnecessary uploads.
Here's a detailed blog on Aruvix: https://dev.to/amrishkhan05/aruvix-the-ultimate-offline-first-developer-toolkit-e0i
You can follow my work and thoughts here:
Portfolio: https://www.amrishkhan.dev/
LinkedIn: https://www.linkedin.com/in/amrishkhan
GitHub: https://www.github.com/amrishkhan05
If you enjoyed this article, consider following for more deep dives into JavaScript, architecture, local-first software, and performance engineering.
Top comments (0)