DEV Community

Cover image for Reduce Has Nothing To Do With Arrays
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

Reduce Has Nothing To Do With Arrays

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

const total = [10, 20, 30]
  .reduce((sum, n) => sum + n, 0)
Enter fullscreen mode Exit fullscreen mode

or

const longestWord = words.reduce(...)
Enter fullscreen mode Exit fullscreen mode

or

const max = numbers.reduce(...)
Enter fullscreen mode Exit fullscreen mode

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
  },
  {}
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In code:

(state, input) => nextState
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's process some actions.

const actions = [
  { type: "INCREMENT" },
  { type: "INCREMENT" },
  { type: "DECREMENT" }
]

const result = actions.reduce(
  reducer,
  0
)

console.log(result)
// 1
Enter fullscreen mode Exit fullscreen mode

Look closely.

Redux's core architecture is simply:

Current State
+
Action
=
Next State
Enter fullscreen mode Exit fullscreen mode

Which is exactly:

(state, action) => nextState
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The React team could have named it anything.

They could have called it:

useStateMachine
useStore
useTransition
useActionState
Enter fullscreen mode Exit fullscreen mode

But they didn't.

They called it:

useReducer
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Again.


State Machines Are Just Reducers

Let's build a traffic light.

States:

RED
GREEN
YELLOW
Enter fullscreen mode Exit fullscreen mode

Events:

NEXT
Enter fullscreen mode Exit fullscreen mode

Reducer:

function reducer(state, event) {
  switch (state) {
    case "RED":
      return "GREEN"

    case "GREEN":
      return "YELLOW"

    case "YELLOW":
      return "RED"
  }
}
Enter fullscreen mode Exit fullscreen mode

Process events:

const events = [
  "NEXT",
  "NEXT",
  "NEXT"
]

const finalState =
  events.reduce(
    reducer,
    "RED"
  )

console.log(finalState)
// RED
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

Reducer:

function accountReducer(
  balance,
  event
) {
  switch (event.type) {
    case "DEPOSIT":
      return balance + event.amount

    case "WITHDRAW":
      return balance - event.amount

    default:
      return balance
  }
}
Enter fullscreen mode Exit fullscreen mode

Current balance:

const balance =
  events.reduce(
    accountReducer,
    0
  )

console.log(balance)
// 125
Enter fullscreen mode Exit fullscreen mode

Most developers would call this:

A reduce operation
Enter fullscreen mode Exit fullscreen mode

An Event Sourcing architect would call this:

State reconstruction
Enter fullscreen mode Exit fullscreen mode

They are the same thing.

Event Sourcing is simply:

Replay events
through a reducer
to reconstruct state.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A projection is simply another reducer.

For example:

function ordersByCustomer(
  state,
  orderCreated
) {
  const {
    customerId
  } = orderCreated

  state[customerId] ??= []

  state[customerId].push(
    orderCreated
  )

  return state
}
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

requires all data to exist upfront.

But streams never end.

So RxJS introduced:

scan()
Enter fullscreen mode Exit fullscreen mode

Example:

interval(1000)
  .pipe(
    scan(
      count => count + 1,
      0
    )
  )
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
3
4
5
6
...
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or:

(state, input) => nextState
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

for every transformation.

I did that too.

Then I stopped.

Because for most day-to-day code:

for (const user of users) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

is clearer.

Often faster.

Usually easier to debug.

Example:

const usersById = {}

for (const user of users) {
  usersById[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode

versus:

const usersById =
  users.reduce(
    (state, user) => {
      state[user.id] = user
      return state
    },
    {}
  )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

There is also another common mistake.

This:

const result =
  users.reduce(
    (state, user) => ({
      ...state,
      [user.id]: user
    }),
    {}
  )
Enter fullscreen mode Exit fullscreen mode

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
    },
    {}
  )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

and start asking:

What transformed this state?
Enter fullscreen mode Exit fullscreen mode

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)