A few years ago, I was obsessed with reduce().
Every problem looked like a reduction problem.
Need a lookup table?
users.reduce(...)
Enter fullscreen mode Exit fullscreen mode
Need grouping?
users.reduce(...)
Enter fullscreen mode Exit fullscreen mode
Need filtering?
users.reduce(...)
Enter fullscreen mode Exit fullscreen mode
Need transformation?
users.reduce(...)
Enter fullscreen mode Exit fullscreen mode
Need world peace?
Probably:
world.reduce(...)
Enter fullscreen mode Exit fullscreen mode
Eventually I realized something.
The problem wasn’t reduce().
The problem was me.
Because while reduce() is incredibly powerful, many developers eventually reach a stage where they start asking a more important question:
Should I even be using
reduce()here?
And surprisingly often, the answer is:
No.
Enter fullscreen mode Exit fullscreen mode
Sometimes a simple:
for...of
Enter fullscreen mode Exit fullscreen mode
is easier to read.
Easier to debug.
Faster.
And ultimately the better engineering choice.
The Problem With Loving reduce() Too Much
Developers often discover:
reduce()
Enter fullscreen mode Exit fullscreen mode
and suddenly realize:
It can do everything.
Enter fullscreen mode Exit fullscreen mode
And that’s true.
The problem is:
Can do everything
≠
Should do everything
Enter fullscreen mode Exit fullscreen mode
Consider:
const usersById =
users.reduce(
(acc, user) => {
acc[user.id] = user
return acc
},
{}
)
Enter fullscreen mode Exit fullscreen mode
Reasonable.
Many teams would accept this.
Now consider:
const usersById = {}
for (const user of users) {
usersById[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode
Question:
Which one is easier to understand?
Enter fullscreen mode Exit fullscreen mode
For many developers:
The loop.
Cognitive Load Matters
Most discussions around reduce focus on:
Elegance
Composability
Functional Style
Enter fullscreen mode Exit fullscreen mode
But software engineering has another important metric:
Cognitive Load
Enter fullscreen mode Exit fullscreen mode
How much mental effort does it take to understand the code?
Reading A Loop
Most developers instantly understand:
const result = {}
for (const user of users) {
result[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode
Mental model:
Create object
Loop
Add items
Done
Enter fullscreen mode Exit fullscreen mode
Reading A Reduce
Now:
const result =
users.reduce(
(acc, user) => {
acc[user.id] = user
return acc
},
{}
)
Enter fullscreen mode Exit fullscreen mode
You must understand:
Accumulator
Initial State
Return Value
Reducer Contract
Enter fullscreen mode Exit fullscreen mode
Not difficult.
But undeniably more concepts.
The “Everything Is Reduce” Trap
Consider:
const activeUserNames =
users.reduce(
(acc, user) => {
if (user.active) {
acc.push(user.name)
}
return acc
},
[]
)
Enter fullscreen mode Exit fullscreen mode
Works.
But compare:
const activeUserNames = []
for (const user of users) {
if (user.active) {
activeUserNames.push(
user.name
)
}
}
Enter fullscreen mode Exit fullscreen mode
Which is clearer?
Many developers would argue:
The loop.
Enter fullscreen mode Exit fullscreen mode
And that’s okay.
The Hidden Mutation Problem
Ironically, many developers use reduce for functional purity.
Then immediately write:
acc.push(...)
Enter fullscreen mode Exit fullscreen mode
or:
acc[user.id] = user
Enter fullscreen mode Exit fullscreen mode
which mutates the accumulator.
Example:
users.reduce(
(acc, user) => {
acc.push(user.name)
return acc
},
[]
)
Enter fullscreen mode Exit fullscreen mode
At this point:
You're already mutating.
Enter fullscreen mode Exit fullscreen mode
So the purity argument disappears.
Now we’re mainly comparing:
Reduce
vs
Loop
Enter fullscreen mode Exit fullscreen mode
on readability and performance.
The Immutable Version Is Often Worse
Some developers try:
users.reduce(
(acc, user) => [
...acc,
user.name
],
[]
)
Enter fullscreen mode Exit fullscreen mode
Looks functional.
But now:
New Array
Every Iteration
Enter fullscreen mode Exit fullscreen mode
Which can become extremely expensive.
Especially on large datasets.
Debugging Matters
Imagine debugging:
users.reduce(
(acc, user) => {
...
},
{}
)
Enter fullscreen mode Exit fullscreen mode
versus:
for (const user of users) {
...
}
Enter fullscreen mode Exit fullscreen mode
Where do most developers place breakpoints?
Usually:
Inside the loop.
Enter fullscreen mode Exit fullscreen mode
Loops are naturally procedural.
Debuggers love procedural code.
Performance Matters Too
From the previous article:
Functional Code Can Be Slower
Enter fullscreen mode Exit fullscreen mode
Many reduce implementations involve:
- callbacks
- closures
- allocations
- intermediate objects
Example:
users.reduce(
(acc, user) => ({
...acc,
[user.id]: user
}),
{}
)
Enter fullscreen mode Exit fullscreen mode
This looks elegant.
But benchmarks terribly.
Compare:
const result = {}
for (const user of users) {
result[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode
Usually much faster.
When reduce() Shines
This article is not anti-reduce.
Far from it.
There are scenarios where reduce is clearly superior.
Scenario 1: Mathematical Aggregation
const total =
prices.reduce(
(sum, price) =>
sum + price,
0
)
Enter fullscreen mode Exit fullscreen mode
Perfect.
Scenario 2: State Evolution
events.reduce(
reducer,
initialState
)
Enter fullscreen mode Exit fullscreen mode
This is literally what reducers were designed for.
Scenario 3: Event Sourcing
From the previous article:
events.reduce(
accountReducer,
initialState
)
Enter fullscreen mode Exit fullscreen mode
Excellent fit.
Scenario 4: Generic Reduction Logic
reduce()
Enter fullscreen mode Exit fullscreen mode
expresses intent very clearly.
You’re reducing many values into one.
When for…of Shines
Scenario 1: Building Objects
const map = {}
for (const item of items) {
map[item.id] = item
}
Enter fullscreen mode Exit fullscreen mode
Clear.
Fast.
Obvious.
Scenario 2: Complex Conditional Logic
for (const user of users) {
if (...) {
...
} else if (...) {
...
}
}
Enter fullscreen mode Exit fullscreen mode
Much easier to follow.
Scenario 3: Multiple Side Effects
for (const order of orders) {
updateCache(order)
logOrder(order)
notify(order)
}
Enter fullscreen mode Exit fullscreen mode
Trying to force this into reduce often becomes awkward.
Scenario 4: Early Exit
for (const user of users) {
if (user.id === targetId) {
break
}
}
Enter fullscreen mode Exit fullscreen mode
Reduce cannot naturally stop.
Loops can.
The Senior Developer Perspective
Junior developers often ask:
Which is better?
Enter fullscreen mode Exit fullscreen mode
Senior developers ask:
Which is clearer?
Enter fullscreen mode Exit fullscreen mode
Because maintainability usually dominates.
Not cleverness.
Not elegance.
Not theoretical purity.
The Most Important Rule
Don’t optimize for:
Shortest Code
Enter fullscreen mode Exit fullscreen mode
Optimize for:
Most Understandable Code
Enter fullscreen mode Exit fullscreen mode
Future you will thank you.
Your teammates will thank you.
Your debugger will thank you.
Pros Of reduce()
1. Expresses Reduction Clearly
2. Great For State Evolution
3. Excellent For Event Processing
4. Composable
5. Powerful Abstraction
Cons Of reduce()
1. Higher Cognitive Load
2. Easy To Abuse
3. Can Hide Complexity
4. Harder To Debug
5. Often Slower Than Loops
Pros Of for…of
1. Extremely Readable
2. Easy To Debug
3. Supports Early Exit
4. Usually Faster
5. Lower Cognitive Overhead
Cons Of for…of
1. Less Declarative
2. Easier To Introduce Side Effects
3. More Boilerplate
4. Harder To Compose
5. Can Become Procedural Spaghetti
If misused.
The Real Lesson
The biggest mistake developers make with reduce() is treating it like a badge of sophistication.
It’s not.
It’s a tool.
A very powerful tool.
But still a tool.
Sometimes:
reduce()
Enter fullscreen mode Exit fullscreen mode
is exactly right.
Sometimes:
for...of
Enter fullscreen mode Exit fullscreen mode
is exactly right.
The best engineers don’t pick sides.
They pick the clearest solution for the problem in front of them.
And surprisingly often, that solution looks a lot less clever than you might expect.
What’s Next?
In the next article we’ll discuss:
The Hidden Cost Of Object Spread
Because one innocent-looking pattern:
{
...obj,
newValue
}
Enter fullscreen mode Exit fullscreen mode
has probably caused more accidental JavaScript performance issues than most developers realize.
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.
답글 남기기