Overview
Applies an accumulator (or “reducer”) function to each value from the source Observable. Like reduce, but emits the current accumulation state after processing each value, rather than waiting until the source completes.
Think of scan as reduce that shows you the work in progress. It’s perfect for maintaining running totals, building up state, or implementing accumulation patterns.
Type Signature
function scan < V , A = V >(
accumulator : ( acc : A | V , value : V , index : number ) => A
) : OperatorFunction < V , V | A >
function scan < V , A >(
accumulator : ( acc : A , value : V , index : number ) => A ,
seed : A
) : OperatorFunction < V , A >
function scan < V , A , S >(
accumulator : ( acc : A | S , value : V , index : number ) => A ,
seed : S
) : OperatorFunction < V , A >
Parameters
accumulator
(acc: A, value: V, index: number) => A
required
A “reducer function” called for each source value. Receives:
acc: The accumulated value (either the seed or the previous result)
value: The current value from the source
index: The zero-based index of the emission
Must return the next accumulated value.
Optional initial accumulation value. If provided, it’s used as the initial state and the first source value goes through the accumulator. If omitted, the first source value is used as the initial state and emitted without going through the accumulator.
Returns
return
OperatorFunction<V, V | A>
A function that returns an Observable of accumulated values. Each emission represents the current state of accumulation.
Usage Examples
Basic Example: Running Total
Sum Without Seed
Sum With Seed
import { of , scan } from 'rxjs' ;
const numbers = of ( 1 , 2 , 3 , 4 , 5 );
const runningTotal = numbers . pipe (
scan (( acc , value ) => acc + value )
);
runningTotal . subscribe ( x => console . log ( x ));
// Output:
// 1 (first value, no accumulation)
// 3 (1 + 2)
// 6 (3 + 3)
// 10 (6 + 4)
// 15 (10 + 5)
Running Average
import { of , scan , map } from 'rxjs' ;
const numbers = of ( 1 , 2 , 3 , 4 , 5 );
numbers . pipe (
scan (( acc , value ) => acc + value , 0 ),
map (( sum , index ) => sum / ( index + 1 ))
). subscribe ( avg => console . log ( 'Average:' , avg ));
// Output:
// Average: 1 (1/1)
// Average: 1.5 (3/2)
// Average: 2 (6/3)
// Average: 2.5 (10/4)
// Average: 3 (15/5)
Building Arrays
import { interval , scan , take } from 'rxjs' ;
interval ( 1000 ). pipe (
take ( 5 ),
scan (( acc , value ) => [ ... acc , value ], [] as number [])
). subscribe ( array => console . log ( array ));
// Output:
// [0]
// [0, 1]
// [0, 1, 2]
// [0, 1, 2, 3]
// [0, 1, 2, 3, 4]
Fibonacci Sequence
import { interval , scan , map , startWith } from 'rxjs' ;
const firstTwoFibs = [ 0 , 1 ];
const fibonacci$ = interval ( 1000 ). pipe (
scan (([ a , b ]) => [ b , a + b ], firstTwoFibs ),
map (([, n ]) => n ),
startWith ( ... firstTwoFibs )
);
fibonacci$ . subscribe ( n => console . log ( n ));
// Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Marble Diagram
Source: --1--2--3--4--5--|
scan((acc, v) => acc + v, 0)
Result: --1--3--6--10-15-|
Common Use Cases
Running Totals : Sum, count, or aggregate values over time
State Management : Build up application state from events
Collecting Results : Accumulate items into collections
Running Calculations : Moving averages, statistics
Event History : Build a history of events
Redux-Style Reducers : Implement state machines
The key difference between scan and reduce: scan emits intermediate results, reduce only emits the final result when the source completes.
import { Subject , scan , map } from 'rxjs' ;
interface CartItem {
id : string ;
name : string ;
price : number ;
quantity : number ;
}
interface CartState {
items : CartItem [];
total : number ;
itemCount : number ;
}
type CartAction =
| { type : 'ADD_ITEM' ; item : CartItem }
| { type : 'REMOVE_ITEM' ; id : string }
| { type : 'UPDATE_QUANTITY' ; id : string ; quantity : number }
| { type : 'CLEAR' };
const actions$ = new Subject < CartAction >();
const initialState : CartState = {
items: [],
total: 0 ,
itemCount: 0
};
const cart$ = actions$ . pipe (
scan (( state : CartState , action : CartAction ) : CartState => {
switch ( action . type ) {
case 'ADD_ITEM' : {
const existingIndex = state . items . findIndex (
item => item . id === action . item . id
);
let items : CartItem [];
if ( existingIndex >= 0 ) {
items = [ ... state . items ];
items [ existingIndex ] = {
... items [ existingIndex ],
quantity: items [ existingIndex ]. quantity + action . item . quantity
};
} else {
items = [ ... state . items , action . item ];
}
return calculateTotals ( items );
}
case 'REMOVE_ITEM' : {
const items = state . items . filter ( item => item . id !== action . id );
return calculateTotals ( items );
}
case 'UPDATE_QUANTITY' : {
const items = state . items . map ( item =>
item . id === action . id
? { ... item , quantity: action . quantity }
: item
);
return calculateTotals ( items );
}
case 'CLEAR' :
return initialState ;
default :
return state ;
}
}, initialState )
);
function calculateTotals ( items : CartItem []) : CartState {
const total = items . reduce (
( sum , item ) => sum + item . price * item . quantity ,
0
);
const itemCount = items . reduce (
( count , item ) => count + item . quantity ,
0
);
return { items , total , itemCount };
}
// Subscribe to cart updates
cart$ . subscribe ( state => {
console . log ( 'Cart updated:' , state );
updateUI ( state );
});
// Dispatch actions
actions$ . next ({
type: 'ADD_ITEM' ,
item: { id: '1' , name: 'Widget' , price: 9.99 , quantity: 2 }
});
actions$ . next ({
type: 'ADD_ITEM' ,
item: { id: '2' , name: 'Gadget' , price: 19.99 , quantity: 1 }
});
actions$ . next ({
type: 'UPDATE_QUANTITY' ,
id: '1' ,
quantity: 3
});
Event Counter by Type
import { fromEvent , scan , map , merge } from 'rxjs' ;
interface EventCounts {
clicks : number ;
keypresses : number ;
scrolls : number ;
}
const clicks$ = fromEvent ( document , 'click' ). pipe ( map (() => 'click' ));
const keys$ = fromEvent ( document , 'keypress' ). pipe ( map (() => 'keypress' ));
const scrolls$ = fromEvent ( document , 'scroll' ). pipe ( map (() => 'scroll' ));
const eventCounts$ = merge ( clicks$ , keys$ , scrolls$ ). pipe (
scan (( counts : EventCounts , eventType : string ) => {
switch ( eventType ) {
case 'click' :
return { ... counts , clicks: counts . clicks + 1 };
case 'keypress' :
return { ... counts , keypresses: counts . keypresses + 1 };
case 'scroll' :
return { ... counts , scrolls: counts . scrolls + 1 };
default :
return counts ;
}
}, { clicks: 0 , keypresses: 0 , scrolls: 0 })
);
eventCounts$ . subscribe ( counts => {
console . log ( 'Event counts:' , counts );
updateDashboard ( counts );
});
Building Objects from Stream
import { interval , scan , take , map } from 'rxjs' ;
interface User {
id : number ;
name : string ;
score : number ;
}
const updates = interval ( 1000 ). pipe (
take ( 5 ),
map ( i => ({
id: i ,
name: `User ${ i } ` ,
score: Math . floor ( Math . random () * 100 )
}))
);
const userMap$ = updates . pipe (
scan (( map , user ) => {
map . set ( user . id , user );
return map ;
}, new Map < number , User >()),
map ( map => Array . from ( map . values ()))
);
userMap$ . subscribe ( users => {
console . log ( 'Current users:' , users );
});
Using Index Parameter
import { of , scan } from 'rxjs' ;
of ( 'a' , 'b' , 'c' , 'd' ). pipe (
scan (( acc , value , index ) => {
return ` ${ acc } [ ${ index } : ${ value } ]` ;
}, 'Start:' )
). subscribe ( x => console . log ( x ));
// Output:
// Start: [0:a]
// Start: [0:a] [1:b]
// Start: [0:a] [1:b] [2:c]
// Start: [0:a] [1:b] [2:c] [3:d]
Error Handling
If the accumulator function throws an error, the error is propagated to the subscriber and the Observable terminates. The accumulated state is lost.
import { of , scan , catchError } from 'rxjs' ;
of ( 1 , 2 , 3 , - 1 , 5 ). pipe (
scan (( acc , value ) => {
if ( value < 0 ) {
throw new Error ( 'Negative value not allowed' );
}
return acc + value ;
}, 0 ),
catchError ( err => {
console . error ( 'Error:' , err . message );
return of ( - 1 );
})
). subscribe ( x => console . log ( x ));
// Output: 1, 3, 6, -1 (error caught)
When accumulating into objects or arrays, be mindful of memory usage. Consider limiting the size of accumulated collections or using immutable update patterns.
// Memory-efficient: limit array size
scan (( acc , value ) => {
const newArr = [ ... acc , value ];
// Keep only last 100 items
return newArr . slice ( - 100 );
}, [] as number [])
// Efficient object updates
scan (( state , update ) => ({
... state ,
... update
}), initialState )
reduce - Like scan, but only emits the final accumulated value
expand - Recursively projects values
mergeScan - Like scan but with Observable accumulator
switchScan - Like scan but switches to new inner Observable
bufferCount - Accumulate into arrays of fixed size