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
)
)
And maybe you've thought:
Okay...
It accumulates stuff.
Seems useful.
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)
Output:
10
Simple.
The reducer processes:
1
2
3
4
and eventually produces:
10
But notice something important.
Reduce only works because:
The collection ends.
At some point:
Array Finished
↓
Emit Final Result
That is fundamental.
Reduce needs an ending.
What Happens When Data Never Ends?
Consider:
interval(1000)
Output:
0
1
2
3
4
5
...
When does it end?
Potentially never.
So if we tried:
reduce()
What would happen?
The answer:
Nothing.
Because reduce is waiting for completion.
And completion never arrives.
Enter scan()
RxJS introduced:
scan()
for exactly this problem.
Example:
interval(1000)
.pipe(
scan(
(sum, value) =>
sum + value,
0
)
)
Output:
0
1
3
6
10
15
21
...
Interesting.
Instead of waiting until the end:
scan()
emits every intermediate state.
Visualizing The Difference
Reduce:
1
2
3
4
↓
10
Scan:
1
↓
1
2
↓
3
3
↓
6
4
↓
10
Every intermediate state becomes visible.
The Formula Is Identical
Reduce:
(accumulator, value) =>
nextAccumulator
Scan:
(accumulator, value) =>
nextAccumulator
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
}
}
Sound familiar?
That's Redux.
Now imagine actions arriving over time.
INCREMENT
INCREMENT
INCREMENT
What happens?
0
↓
1
↓
2
↓
3
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()
Reducer:
const reducer = (
state,
action
) => {
switch (action.type) {
case "ADD":
return state + action.value
default:
return state
}
}
Store:
const state$ =
actions$.pipe(
scan(reducer, 0)
)
Dispatch:
actions$.next({
type: "ADD",
value: 5
})
actions$.next({
type: "ADD",
value: 10
})
Output:
5
15
We just built reactive state management using scan.
Event Sourcing Is Also scan()
Consider a bank account.
Events:
Deposit 100
Deposit 50
Withdraw 25
Reducer:
const accountReducer =
(balance, event) => {
switch (event.type) {
case "DEPOSIT":
return balance +
event.amount
case "WITHDRAW":
return balance -
event.amount
}
}
Traditional:
events.reduce(
accountReducer,
0
)
Result:
125
Now imagine events arriving live.
Deposit 100
(wait)
Deposit 50
(wait)
Withdraw 25
Suddenly:
scan()
becomes the natural choice.
Event sourcing becomes a continuous reduction process.
Why scan() Feels Magical
Because it combines two powerful ideas.
First:
State
Second:
Time
The result:
State Evolution Over Time
Which is exactly what most applications are doing.
Real World Example: Shopping Cart
Actions:
Add Product
Add Product
Remove Product
Scan:
cartActions$
.pipe(
scan(
cartReducer,
[]
)
)
Output:
Cart Version 1
Cart Version 2
Cart Version 3
Every state change emitted automatically.
Real World Example: Live Analytics
Visitors arrive.
Visitor
Visitor
Visitor
Visitor
Scan:
visitors$
.pipe(
scan(
count => count + 1,
0
)
)
Output:
1
2
3
4
5
...
Live metrics become trivial.
Real World Example: Game Score
Player scores:
10
20
50
Scan:
score$
.pipe(
scan(
(total, score) =>
total + score,
0
)
)
Output:
10
30
80
Perfect for dashboards.
Performance Considerations
A common misconception:
scan()
stores all previous values
It doesn't.
Normally:
Previous State
+
Current Value
=
New State
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
instead of:
Underlying Concepts
When you memorize:
scan()
it feels random.
When you realize:
scan()
=
reduce()
+
time
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
wasn't really about arrays.
And:
map
wasn't really about arrays either.
The real abstraction is:
Current State
+
New Value
=
Next State
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
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)