scan() Is Reduce For Infinite Data
Source: Dev.to
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()
- Perfect For Infinite Streams
Works where reduce cannot.
- Great For State Management
Redux-like patterns emerge naturally.
- Constant Memory Usage
Only current state is required.
- Easy To Compose
Works beautifully with RxJS pipelines.
- Foundation Of Reactive Architecture
Many reactive systems are built around scan.
Cons Of scan()
- Can Be Misused
Not every stream needs accumulated state.
- State Logic Can Become Complex
Large reducers become difficult to maintain.
- Debugging Deep Pipelines Can Be Hard
Especially when multiple scans exist.
- Requires Thinking In Streams
Which takes time to learn.
- 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:
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.