Hello! Welcome back to our second module, Core Operators and Stream Transformation.
In our last lesson, we learned how to use the tap operator to inspect stream emissions, errors, and completions. This gave us a "window" into our Observables for debugging and performing side effects without altering the stream's data.
Today, we move from passive observation to active transformation. This lesson covers two of the most fundamental and frequently used operators in RxJS: map and scan. Our learning outcome is to transform stream values using map and accumulate state within a stream using scan.
Mastering these two operators is a crucial step toward building complex reactive logic. map is your primary tool for shaping data as it flows, while scan is the cornerstone of building reactive state management systems, a key part of your goal to master RxJS in a React context.
map: Transforming Stream Values
The map operator is likely familiar to you from its counterpart in JavaScript, Array.prototype.map. The principle is identical: it applies a function to every value emitted by a source Observable and emits the result of that function. It's a direct, one-to-one transformation.

Let's start by reading about the core purpose and common use cases of map.
The map page on learnrxjs.io provides an excellent overview of why and when to use this operator.
Read the sections 'Why use map?' and the introductory paragraphs. Focus on its role in reshaping data (like API responses), extracting properties, and the critical distinction between map for synchronous transformations and flattening operators (like switchMap) for asynchronous ones.
As the resource highlights, map is your go-to for synchronous data shaping. Some common scenarios include:
- Extracting a property from an object:
map(user => user.name) - Performing a calculation:
map(price => price * 1.2) - Reshaping an API response for the UI: This is a very common pattern in front-end development.
Here are a few code examples from the same resource that illustrate these uses.
These examples from learnrxjs.io demonstrate the practical application of map in common scenarios.
Review 'Example 2: Map to single property', 'Example 3: Mapping API Response to UI Model', and 'Example 4: Transform DOM Events'. Notice how map is used to simplify complex objects into the exact data structure your application needs.
An Important Distinction: RxJS map vs. Array.prototype.map
A common point of confusion arises when a stream emits an array. The RxJS map operator acts on the entire array as a single emission. If you want to transform the elements inside the array, you must use the standard Array.prototype.map inside the RxJS map operator.
This is a subtle but critical point for writing clean, correct code. The following video explains this distinction very clearly.
I only ever use *these* RxJS operators to code reactively
In this video, Joshua Morony provides a clear, practical explanation of map and filter, and specifically addresses the confusion between the RxJS operator and the array method.
Watch the section 'Understanding Map and Filter Operators' from 01:51 to 05:06. Pay close attention to the explanation starting around 03:20 where he differentiates between the two map functions when dealing with an array emission.
To summarize this key concept:
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
// Source emits an array as a single value
const source$ = of([1, 2, 3]);
// INCORRECT: This tries to multiply the array itself by 10, resulting in NaN.
source$.pipe(
map(value => value * 10)
).subscribe(console.log); // Logs: NaN
// CORRECT: Use RxJS map to access the emitted array, then Array.map to transform its contents.
source$.pipe(
map(arr => arr.map(num => num * 10))
).subscribe(console.log); // Logs: [10, 20, 30]
scan: Accumulating State Over Time
While map performs a stateless, one-to-one transformation, scan introduces state. It's the reactive version of Array.prototype.reduce. However, there's a crucial difference:
reduceprocesses the entire source and emits only the single, final accumulated value upon completion.scanemits the current accumulated value for every emission from the source.
This "emit-on-the-go" behavior makes scan incredibly powerful for managing state.
Let's read a bit more about this powerful operator.
The scan page on learnrxjs.io explains its core functionality and directly connects it to state management patterns.
Read the section 'Why use scan?'. Note the phrase 'accumulate and emit on-the-go' and the explicit mention of creating Redux-like state management, which is highly relevant to your goals.
The signature for scan is scan(accumulatorFn, seed).
accumulatorFn: A function that takes the previous accumulated value (acc) and the current value (curr) and returns the new accumulated value.seed: The initial value of the accumulator.
Here's a basic example:
import { of } from 'rxjs';
import { scan } from 'rxjs/operators';
const source$ = of(1, 2, 3, 4);
source$.pipe(
scan((acc, curr) => acc + curr, 0) // Start with a seed of 0
).subscribe(console.log);
// Output:
// 1 (0 + 1)
// 3 (1 + 2)
// 6 (3 + 3)
// 10 (6 + 4)
Using scan for State Management
This ability to build up a value over time makes scan the perfect tool for managing component or application state within a stream. Instead of just numbers, the accumulator can be an array or an object representing your state.
The following video starts with a simple state accumulation example and then builds to a more advanced, scalable pattern that is excellent for real-world applications.
RxJS Scan Operator - How to Manage the State
This video from Decoded Frontend provides a fantastic, practical guide to using scan for state management, progressing from a basic to an advanced pattern.
First, watch from 03:08 to 05:51 to see how scan can be used to accumulate click events into an array. Then, watch the more advanced section from 09:35 to 14:22. This second part introduces a powerful pattern where the stream emits handler functions that know how to update the state. This is a very clean and scalable way to manage different types of state transitions (e.g., add, remove, reset).
Let's break down that advanced pattern. Instead of putting complex if/else or switch logic inside your scan operator to handle different actions, you can do the following:
- Create separate streams for each "action" (e.g., an "add item" stream, a "reset" stream).
- Use
mapon each action stream to transform the incoming event into a state update function. For example, the "add item" stream emits functions of the formstate => [...state, newItem]. The "reset" stream emits functions likestate => []. mergethese streams of functions into a single stream.- Pipe this merged stream into
scan. The accumulator function inscanbecomes incredibly simple: it just calls the incoming function with the current state:(state, updateFn) => updateFn(state).
Here is a conceptual code example of that pattern:
import { fromEvent, merge, Subject } from 'rxjs';
import { map, scan } from 'rxjs/operators';
// State and Action definitions
type State = string[];
type StateUpdateFn = (state: State) => State;
// Action sources
const addButton = document.getElementById('add');
const resetButton = document.getElementById('reset');
const itemInput = document.getElementById('item-input') as HTMLInputElement;
// --- Action streams mapped to StateUpdateFns ---
const addItem$ = fromEvent(addButton, 'click').pipe(
map(() => {
const newItem = itemInput.value;
// Return a function that knows how to add an item
return (state: State): State => [...state, newItem];
})
);
const reset$ = fromEvent(resetButton, 'click').pipe(
// Return a function that knows how to reset the state
map((): StateUpdateFn => (state: State): State => [])
);
// --- State Stream ---
// Merge all action streams into one stream of update functions
const state$ = merge(addItem$, reset$).pipe(
// The seed is our initial state
scan((state: State, updateFn: StateUpdateFn) => updateFn(state), [])
);
// Subscribe to see the state evolve
state$.subscribe(currentState => {
console.log('Current State:', currentState);
});
This pattern is highly scalable and promotes separation of concerns. Each piece of state logic is encapsulated in its own function, making the system easier to test, debug, and extend. This is a powerful technique you'll see again when we discuss Redux-like patterns.
Conclusion
In this lesson, we've covered two of the most important operators for manipulating data in RxJS:
map: For stateless, synchronous, one-to-one transformation of values. It's your tool for shaping data into the format you need.scan: For stateful accumulation. It processes a stream over time, emitting each intermediate result, making it the foundation for reactive state management.
You now have the ability not just to create and observe streams, but to fundamentally change the data within them and build up state over time.
In our next lesson, we will explore filtering operators. We'll learn how to control the flow of data using filter, take, debounceTime, and throttleTime, allowing you to selectively decide which emissions should proceed down the stream.
Can't find a good explanation? Sign up and we'll make it for you
Sign up