DEV Community

Cover image for RxJS in Angular — Chapter 3 | pipe()` and Operators — The Superpowers of RxJS
Jack Pritom Soren
Jack Pritom Soren

Posted on

RxJS in Angular — Chapter 3 | pipe()` and Operators — The Superpowers of RxJS

"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:

  1. Squeeze it (transform the data)
  2. Filter out the seeds (filter the data)
  3. 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 🍹
Enter fullscreen mode Exit fullscreen mode

🔧 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
  });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        })))
      );
  }
}
Enter fullscreen mode Exit fullscreen mode
// 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) {}
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

👀 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
// ...
Enter fullscreen mode Exit fullscreen mode

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`))
    );
}
Enter fullscreen mode Exit fullscreen mode
// 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;
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

💡 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!
  });
Enter fullscreen mode Exit fullscreen mode

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))
      );
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

🎯 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 reaches subscribe()
  • 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)