"pipe() and Operators β The Superpowers of RxJS"
π Welcome to Chapter 3!
So far you know:
- Observable = a stream of data (Chapter 1)
- subscribe = how you receive data (Chapter 2)
Now here's the real magic: What if you want to transform, filter, or modify the data before it reaches you?
That's what pipe() and operators are for!
π Think of it Like Making Lemonade
You have a lemon π (raw data from an API).
You don't want to eat a raw lemon. You want to:
- Squeeze it (transform the data)
- Filter out the seeds (filter the data)
- Add sugar (modify the data)
That's what pipe() does β it's a processing pipeline that your data flows through before reaching your subscribe().
Observable β [pipe: squeeze β filter β add sugar] β subscribe gets lemonade πΉ
π§ What is pipe()?
pipe() is a method on every Observable. You use it to chain operators together.
someObservable
.pipe(
operator1(),
operator2(),
operator3()
)
.subscribe(result => {
console.log(result); // result has been processed by all 3 operators
});
Think of it like an assembly line π:
- Raw data comes in on one end
- Each operator does its job
- Processed data comes out on the other end
- subscribe() picks it up
πΊοΈ Operator #1: map() β Transform Your Data
map() takes every value from the Observable and transforms it into something else.
It's exactly like Array.map() β but for streams.
Simple Example:
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
// of() creates an Observable that emits these values one by one
of(1, 2, 3, 4, 5)
.pipe(
map(number => number * 10) // Multiply every number by 10
)
.subscribe(result => {
console.log(result);
});
// Output:
// 10
// 20
// 30
// 40
// 50
Real Angular Example β Transforming API Response
Imagine the API gives you user data but you only need the name and email:
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface ApiUser {
id: number;
name: string;
username: string;
email: string;
phone: string;
website: string;
address: object; // We don't need this!
company: object; // We don't need this either!
}
interface SimpleUser {
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
// Returns only the fields we need β not the whole messy API response
getSimpleUsers(): Observable<SimpleUser[]> {
return this.http.get<ApiUser[]>('https://jsonplaceholder.typicode.com/users')
.pipe(
map(users => users.map(user => ({
name: user.name,
email: user.email
// We dropped id, phone, website, address, company
})))
);
}
}
// users.component.ts
@Component({
template: `
<div *ngFor="let user of users$ | async">
<strong>{{ user.name }}</strong> β {{ user.email }}
</div>
`
})
export class UsersComponent {
users$ = this.userService.getSimpleUsers();
constructor(private userService: UserService) {}
}
The template only sees name and email β clean and simple! π§Ή
π Operator #2: filter() β Only Let Certain Values Through
filter() is like a bouncer at a club β only values that pass the test get through.
import { of } from 'rxjs';
import { filter } from 'rxjs/operators';
of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.pipe(
filter(number => number % 2 === 0) // Only even numbers
)
.subscribe(n => console.log(n));
// Output: 2, 4, 6, 8, 10
Real Angular Example β Filter Active Products
// product.service.ts
getActiveProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products')
.pipe(
map(products => products.filter(p => p.isActive === true))
// Or using RxJS filter:
// This filters the ARRAY inside the stream
);
}
Or if the API emits products one at a time:
// Filter individual emissions
productStream$
.pipe(
filter(product => product.price > 0), // Skip free products
filter(product => product.stock > 0) // Skip out-of-stock
)
.subscribe(product => {
this.availableProducts.push(product);
});
π Operator #3: tap() β Peek Without Changing Anything
tap() lets you look at the data as it flows through the pipe without modifying it.
It's perfect for debugging or doing side effects (like showing a loading spinner, logging, etc.)
import { of } from 'rxjs';
import { tap, map } from 'rxjs/operators';
of(1, 2, 3)
.pipe(
tap(n => console.log('Before map:', n)), // Peek at raw value
map(n => n * 10),
tap(n => console.log('After map:', n)) // Peek at transformed value
)
.subscribe(n => console.log('Final:', n));
// Output:
// Before map: 1
// After map: 10
// Final: 10
// Before map: 2
// After map: 20
// Final: 20
// ...
Real Angular Example β Loading State with tap
// user.service.ts
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users')
.pipe(
tap(() => console.log('HTTP request made!')), // Debugging
tap(users => console.log(`Received ${users.length} users`))
);
}
// users.component.ts
@Component({
template: `
<div *ngIf="isLoading">Loading... β³</div>
<ul *ngIf="!isLoading">
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`
})
export class UsersComponent implements OnInit {
users: User[] = [];
isLoading = false;
ngOnInit() {
this.isLoading = true;
this.userService.getUsers()
.pipe(
tap(() => {
// Side effect: set loading to false when data arrives
this.isLoading = false;
})
)
.subscribe(users => {
this.users = users;
});
}
}
π‘ Golden Rule of
tap(): It never changes the data. It's just for peeking and side effects.
π’ Chaining Multiple Operators
The real power is when you chain operators together:
this.http.get<Product[]>('/api/products')
.pipe(
tap(() => console.log('Fetching products...')), // 1. Log it
map(products => products.filter(p => p.active)), // 2. Only active
map(products => products.sort((a, b) => // 3. Sort by price
a.price - b.price
)),
tap(products => console.log(`${products.length} products ready`)) // 4. Log result
)
.subscribe(products => {
this.products = products; // 5. Clean, sorted, active products!
});
Data flows through each operator like water through filters π§
πͺ Full Real-World Example β Product Catalog Page
Let's build a complete product catalog that uses pipe, map, filter, and tap:
product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter, tap } from 'rxjs/operators';
export interface Product {
id: number;
name: string;
price: number;
category: string;
inStock: boolean;
rating: number;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
constructor(private http: HttpClient) {}
getFeaturedProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products')
.pipe(
// Step 1: Log for debugging
tap(products => console.log('Raw data:', products.length, 'items')),
// Step 2: Only get in-stock products
map(products => products.filter(p => p.inStock)),
// Step 3: Only get high-rated products (4 stars and above)
map(products => products.filter(p => p.rating >= 4)),
// Step 4: Sort by rating, highest first
map(products => [...products].sort((a, b) => b.rating - a.rating)),
// Step 5: Take only top 10
map(products => products.slice(0, 10)),
// Step 6: Log final result
tap(products => console.log('Featured products ready:', products.length))
);
}
}
product-catalog.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Product, ProductService } from './product.service';
@Component({
selector: 'app-product-catalog',
template: `
<h1>β Featured Products</h1>
<div class="product-grid">
<div *ngFor="let product of products$ | async" class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">ΰ§³{{ product.price }}</p>
<p class="category">{{ product.category }}</p>
<div class="rating">
β {{ product.rating }}/5
</div>
<button>Add to Cart π</button>
</div>
</div>
<p *ngIf="(products$ | async)?.length === 0">
No featured products available right now.
</p>
`
})
export class ProductCatalogComponent {
products$: Observable<Product[]>;
constructor(private productService: ProductService) {
this.products$ = this.productService.getFeaturedProducts();
}
}
Clean, readable, and all the messy filtering/sorting logic is in the service β not the template!
π§© More Useful Operators β Quick Overview
Here are a few more operators you'll use frequently:
take(n) β Only Take First N Values
import { interval } from 'rxjs';
import { take } from 'rxjs/operators';
// interval() emits 0, 1, 2, 3... every second FOREVER
// take(5) stops it after 5 values
interval(1000)
.pipe(take(5))
.subscribe(n => console.log(n));
// Output: 0, 1, 2, 3, 4 (then automatically completes!)
distinctUntilChanged() β Skip Duplicate Values
import { of } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
// Great for search inputs β don't search if the value hasn't changed
of('apple', 'apple', 'banana', 'banana', 'cherry')
.pipe(distinctUntilChanged())
.subscribe(v => console.log(v));
// Output: apple, banana, cherry (duplicates skipped!)
debounceTime() β Wait Before Processing
import { debounceTime } from 'rxjs/operators';
// Wait 300ms after the last keystroke before searching
searchInput.valueChanges
.pipe(debounceTime(300))
.subscribe(value => this.search(value));
catchError() β Handle Errors Gracefully
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
this.http.get('/api/users')
.pipe(
catchError(error => {
console.error('API failed:', error);
return of([]); // Return empty array instead of crashing
})
)
.subscribe(users => this.users = users);
π― When to Use What
map() β Anytime you want to transform or reshape data from the API
"I got raw API data, let me turn it into what my component needs"
filter() β When you want to skip certain values
"Only show me products that are in stock"
tap() β For debugging or side effects that shouldn't change data
"Log this, show a spinner, but don't touch the actual data"
take(n) β When you only need the first few values
"I only need the first response, then stop"
catchError() β Always use this for HTTP calls in production apps
"If the API breaks, show a nice message instead of crashing"
π§ Chapter 3 Summary β What You Learned
-
pipe()is an assembly line that processes data before it reachessubscribe() -
map()transforms every value into something new -
filter()lets only certain values through -
tap()lets you peek at data or do side effects without changing anything - You can chain multiple operators inside one
pipe() - Common patterns: use
map()for data transformation,filter()for filtering arrays,tap()for logging and side effects
π Coming Up in Chapter 4...
We've covered the basics of operators. Now it's time for one of the most confusing yet important operators in all of RxJS:
switchMap, mergeMap, and concatMap β the "flattening operators" that handle Observables inside Observables.
These are used in almost every real Angular app. Don't miss it!
See you in Chapter 4! π
π RxJS Deep Dive Newsletter Series | Chapter 3 of 10
Follow me on : Github Linkedin Threads Youtube Channel
Top comments (0)