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)