DEV Community

Cover image for scan() Is Reduce For Infinite Data
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

scan() Is Reduce For Infinite Data

If you've used RxJS for any meaningful amount of time, you've probably encountered scan().

Maybe you've seen code like this:

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

And maybe you've thought:

Okay...

It accumulates stuff.

Seems useful.
Enter fullscreen mode Exit fullscreen mode

Then you moved on.

I did too.

For a long time, I treated scan() as just another RxJS operator.

One more thing to memorize.

Then I realized something.

scan() isn't a special RxJS operator.

It's simply reduce() adapted for data that never ends.

And once you understand that, a huge part of reactive programming suddenly becomes much easier.


The Limitation Of reduce()

Let's start with something familiar.

const total =
  [1, 2, 3, 4]
    .reduce(
      (sum, n) => sum + n,
      0
    )

console.log(total)
Enter fullscreen mode Exit fullscreen mode

Output:

10
Enter fullscreen mode Exit fullscreen mode

Simple.

The reducer processes:

1
2
3
4
Enter fullscreen mode Exit fullscreen mode

and eventually produces:

10
Enter fullscreen mode Exit fullscreen mode

But notice something important.

Reduce only works because:

The collection ends.
Enter fullscreen mode Exit fullscreen mode

At some point:

Array Finished
↓
Emit Final Result
Enter fullscreen mode Exit fullscreen mode

That is fundamental.

Reduce needs an ending.


What Happens When Data Never Ends?

Consider:

interval(1000)
Enter fullscreen mode Exit fullscreen mode

Output:

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

When does it end?

Potentially never.

So if we tried:

reduce()
Enter fullscreen mode Exit fullscreen mode

What would happen?

The answer:

Nothing.
Enter fullscreen mode Exit fullscreen mode

Because reduce is waiting for completion.

And completion never arrives.


Enter scan()

RxJS introduced:

scan()
Enter fullscreen mode Exit fullscreen mode

for exactly this problem.

Example:

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

Output:

0
1
3
6
10
15
21
...
Enter fullscreen mode Exit fullscreen mode

Interesting.

Instead of waiting until the end:

scan()
emits every intermediate state.
Enter fullscreen mode Exit fullscreen mode

Visualizing The Difference

Reduce:

1
2
3
4

↓

10
Enter fullscreen mode Exit fullscreen mode

Scan:

1
↓
1

2
↓
3

3
↓
6

4
↓
10
Enter fullscreen mode Exit fullscreen mode

Every intermediate state becomes visible.


The Formula Is Identical

Reduce:

(accumulator, value) =>
  nextAccumulator
Enter fullscreen mode Exit fullscreen mode

Scan:

(accumulator, value) =>
  nextAccumulator
Enter fullscreen mode Exit fullscreen mode

Same function signature.

Same concept.

Different timing.

That is why I like describing scan as:

Reduce for infinite data.


The Hidden Relationship To Redux

This is where things get interesting.

Suppose:

const reducer = (
  state,
  action
) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1

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

Sound familiar?

That's Redux.

Now imagine actions arriving over time.

INCREMENT
INCREMENT
INCREMENT
Enter fullscreen mode Exit fullscreen mode

What happens?

0
↓
1
↓
2
↓
3
Enter fullscreen mode Exit fullscreen mode

That's scan.

Literally.

Redux is conceptually a scan over actions.


State Management Is Just scan()

Let's build a tiny state store.

const actions$ = new Subject()
Enter fullscreen mode Exit fullscreen mode

Reducer:

const reducer = (
  state,
  action
) => {
  switch (action.type) {
    case "ADD":
      return state + action.value

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

Store:

const state$ =
  actions$.pipe(
    scan(reducer, 0)
  )
Enter fullscreen mode Exit fullscreen mode

Dispatch:

actions$.next({
  type: "ADD",
  value: 5
})

actions$.next({
  type: "ADD",
  value: 10
})
Enter fullscreen mode Exit fullscreen mode

Output:

5
15
Enter fullscreen mode Exit fullscreen mode

We just built reactive state management using scan.


Event Sourcing Is Also scan()

Consider a bank account.

Events:

Deposit 100
Deposit 50
Withdraw 25
Enter fullscreen mode Exit fullscreen mode

Reducer:

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

      case "WITHDRAW":
        return balance -
          event.amount
    }
  }
Enter fullscreen mode Exit fullscreen mode

Traditional:

events.reduce(
  accountReducer,
  0
)
Enter fullscreen mode Exit fullscreen mode

Result:

125
Enter fullscreen mode Exit fullscreen mode

Now imagine events arriving live.

Deposit 100
(wait)

Deposit 50
(wait)

Withdraw 25
Enter fullscreen mode Exit fullscreen mode

Suddenly:

scan()
Enter fullscreen mode Exit fullscreen mode

becomes the natural choice.

Event sourcing becomes a continuous reduction process.


Why scan() Feels Magical

Because it combines two powerful ideas.

First:

State
Enter fullscreen mode Exit fullscreen mode

Second:

Time
Enter fullscreen mode Exit fullscreen mode

The result:

State Evolution Over Time
Enter fullscreen mode Exit fullscreen mode

Which is exactly what most applications are doing.


Real World Example: Shopping Cart

Actions:

Add Product
Add Product
Remove Product
Enter fullscreen mode Exit fullscreen mode

Scan:

cartActions$
  .pipe(
    scan(
      cartReducer,
      []
    )
  )
Enter fullscreen mode Exit fullscreen mode

Output:

Cart Version 1
Cart Version 2
Cart Version 3
Enter fullscreen mode Exit fullscreen mode

Every state change emitted automatically.


Real World Example: Live Analytics

Visitors arrive.

Visitor
Visitor
Visitor
Visitor
Enter fullscreen mode Exit fullscreen mode

Scan:

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

Output:

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

Live metrics become trivial.


Real World Example: Game Score

Player scores:

10
20
50
Enter fullscreen mode Exit fullscreen mode

Scan:

score$
  .pipe(
    scan(
      (total, score) =>
        total + score,
      0
    )
  )
Enter fullscreen mode Exit fullscreen mode

Output:

10
30
80
Enter fullscreen mode Exit fullscreen mode

Perfect for dashboards.


Performance Considerations

A common misconception:

scan()
stores all previous values
Enter fullscreen mode Exit fullscreen mode

It doesn't.

Normally:

Previous State
+
Current Value
=
New State
Enter fullscreen mode Exit fullscreen mode

Only the accumulator survives.

Memory usage remains constant.

Which makes scan suitable for:

  • Live streams
  • WebSockets
  • Telemetry
  • Monitoring
  • Analytics

Why Most Developers Misunderstand scan()

Because they learn:

Operator Names
Enter fullscreen mode Exit fullscreen mode

instead of:

Underlying Concepts
Enter fullscreen mode Exit fullscreen mode

When you memorize:

scan()
Enter fullscreen mode Exit fullscreen mode

it feels random.

When you realize:

scan()
=
reduce()
+
time
Enter fullscreen mode Exit fullscreen mode

it becomes obvious.


Pros Of scan()

1. Perfect For Infinite Streams

Works where reduce cannot.


2. Great For State Management

Redux-like patterns emerge naturally.


3. Constant Memory Usage

Only current state is required.


4. Easy To Compose

Works beautifully with RxJS pipelines.


5. Foundation Of Reactive Architecture

Many reactive systems are built around scan.


Cons Of scan()

1. Can Be Misused

Not every stream needs accumulated state.


2. State Logic Can Become Complex

Large reducers become difficult to maintain.


3. Debugging Deep Pipelines Can Be Hard

Especially when multiple scans exist.


4. Requires Thinking In Streams

Which takes time to learn.


5. Easy To Confuse With reduce()

The names are similar.

The behavior is not.


The Real Lesson

The biggest lesson I learned about scan() was that it wasn't really an RxJS operator.

Just like:

reduce
Enter fullscreen mode Exit fullscreen mode

wasn't really about arrays.

And:

map
Enter fullscreen mode Exit fullscreen mode

wasn't really about arrays either.

The real abstraction is:

Current State
+
New Value
=
Next State
Enter fullscreen mode Exit fullscreen mode

Reduce applies that idea to finite collections.

Scan applies that idea to data that keeps arriving.

Once you understand that relationship, RxJS stops feeling like a collection of mysterious operators.

It starts feeling like a natural extension of concepts you already know.

And that is usually the moment reactive programming finally clicks.


What's Next?

In the next article we'll leave RxJS behind and move into software architecture:

Event Sourcing Is Just Reduce Over Time

Because once you understand:

reduce()
scan()
state evolution
Enter fullscreen mode Exit fullscreen mode

you're only one step away from understanding the architecture behind some of the most scalable systems ever built.


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)