RxJS Operator Essentials: Map, Tap, Filter, and More

RxJS Operator
Image by Dah Lohmar On UnSplash

Introduction

If you've been working with Angular for a while and find some RxJS pipeable operators slightly confusing, we will explore these familiar operators here.

Great, let’s dive into this exciting topic.

What is an RxJS Operator?

RxJS ships with more than 100 operators.

These operators are one of the building blocks of RxJS.

It is handy for manipulating (process or transform) streams, which are data sequences over time.

Remember that operators are functions, and there are two types of operators.

Let’s see them one by one.

RxJS map Operator

The map function in RxJS is an operator that transforms each emitted value from an observable based on a provided function, returning a new observable with modified values.

Let us see an example.

Creation Operators

We won’t be exploring the creation operator, but it is essential to give an overview.

Creation operators are functions that can create an Observable with some expected predefined behavior or by joining other Observables.

Here are some familiar creation operators: of, from, fromEvent, and range.

Let’s have one example.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { from, Subscription } from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
  title = 'RxJSOperators';
  subFrom$!: Subscription;

  public ngOnInit(): void {
    this.subFrom$ = from([2, 4, 6, 8]).subscribe({
      next: (item) => console.log(item),
      error: (err) => console.log(err),
      complete: () => console.log('completed')
    });
  }

  public ngOnDestroy(): void {
    this.subFrom$.unsubscribe();
  }
}

From our code example, we have seen that inside the ngOnInit, the from([2,4,6,8]) method creates an observable that emits each number in the array sequentially.

The subscribe method attaches an observer to this observable.

  • First, next, which logs each item emitted.
  • Second, error logs any errors if there’s one.
  • Third, complete which logs “completed” when the observable completes.

Although this is not the main topic of our article, it is a good thing to have an idea.

Pipeable Operators

Pipeable operators are the kind that can be piped to Observables.

Let’s see the syntax below.

observableInstance.pipe(operator);
or 
observableInstance.pipe(operatorFactory());

Essentially, the operator and operatorFactory behave as a single method in RxJS.

However, there’s a difference between the two.

Operator factories are functions that accept parameters to configure their behavior and immediately return operator functions.

Operator functions are the results of these calls (configured functions), which can then transform the observable’s data inside the .pipe().

Let’s see code examples of pipeable operators in the next sections.

RxJS map Operator

The map function in RxJS is an operator that transforms each emitted value from an observable based on a provided function, returning a new observable with modified values.

Let us see an example.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { from, map, Subscription } from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
  title = 'RxJSOperators';
  subCustomer1!: Subscription;

  public ngOnInit(): void {
    const customer$ = from([
      { id: 1, jerseyNumber: 23, age: 61, fullName: "Michael Jordan" },
      { id: 2, jerseyNumber: 23, age: 43, fullName: "Lebron James" },
      { id: 3, jerseyNumber: 24, age: 89, fullName: "Kobe Bryant" }
    ]);

    this.subCustomer1 = customer$
      .pipe(
        map(item => ({
          ...item, shoe: "Nike"
        }))
      )
      .subscribe((result) => console.log(result, "Added Nike shoes"));
  }

  public ngOnDestroy(): void {
    this.subCustomer1.unsubscribe();
  }
}

From our example, the map operator transforms each customer object by adding a new property, shoe, with a value of "Nike".

Then, each customer object is spread (...item) to retain the existing properties while adding the shoe property, resulting in a new object.

Finally, log the result to the console with a message saying, "Added Nike shoes".

Let’s see the output below.

Output

RxJS tap Operator

The tap function in RxJS is an operator that allows you to perform side effects, like logging or debugging, on each emitted value without altering the observable’s values.

Let us see an example.

import {Component, OnDestroy, OnInit} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {filter, from, map, Subscription, tap} from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements  OnInit, OnDestroy {
  title = 'RxJSOperators';
  subCustomer2!: Subscription; 
  public ngOnInit() {
    const customer$ = from([
							{ id: 1, jerseyNumber: 23, age: 61, fullName: "Michael Jordan"},
							{ id: 2, jerseyNumber: 23, age: 43, fullName: "Lebron James"},
							{ id: 3, jerseyNumber: 24, age: 89, fullName: "Kobe Bryant"}
						   ]);
						   
    this.subCustomer2 = customer$.pipe(
      tap(item => console.log(item, "Debugging this item before putting Nike shoes")),
      map(item => ({
       ...item, shoe: "Nike"
      })),
      tap(item => console.log(item, "Debugging this item after putting Nike shoes")))
      .subscribe((result) => { console.log(result, "Final result"); });
  }
  public ngOnDestroy() {  
    this.subCustomer2.unsubscribe();
  }
}

The first tap operator logs each customer object to the console before transforming.

This is useful for debugging, allowing you to inspect the original data emitted by the customer.

Each item is logged with the message "Debugging this item before putting Nike shoes".

The map operator transforms each customer object by adding a new property, shoe, with a value of "Nike".

As discussed in the previous example, this operation uses object spread (...item) to retain the original properties while adding the new shoe property, resulting in a modified object for each customer.

The second tap operator logs each transformed customer object, allowing you to verify that the shoe property has been successfully added.

Each item is logged with the message "Debugging this item after putting Nike shoes".

Finally, the subscribe method listens to the transformed observable.

For each item, it logs the final transformed result along with the "Final result" to indicate the completion of the transformation.

Let’s see the output below.

Final Result

RxJS filter Operator

The filter function in RxJS is an operator that emits only the values from an observable that passes a specified condition, effectively filtering out values that don’t meet the criteria.

Let us see an example.

import {Component, OnDestroy, OnInit} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {filter, from, map, Subscription, tap} from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements  OnInit, OnDestroy {
  title = 'RxJSOperators';
  subCustomer3!: Subscription;
  
  public ngOnInit() {

    const customer$ = from([
							 { id: 1, jerseyNumber: 23, age: 61, fullName: "Michael Jordan"},
							 { id: 2, jerseyNumber: 23, age: 43, fullName: "Lebron James"},
							 { id: 3, jerseyNumber: 24, age: 89, fullName: "Kobe Bryant"}
						   ]);
						   
    this.subCustomer3 = customer$.
                             pipe(filter( item => item.jerseyNumber === 23)).
                              subscribe((result) => { console.log(result); });

  }
  
  public ngOnDestroy() {

    this.subCustomer3.unsubscribe();
  }
}

The filter(item => item.jerseyNumber === 23) filters the data, allowing only objects where jerseyNumber is 23 to pass through the stream.

In this example, Michael Jordan and LeBron James meet this condition, while Kobe Bryant does not.

Let’s see the output below.

Data

RxJS take Operator

The take function in RxJS emits only the first specified number of values from an observable and then completes.

Let us see an example.

import {Component, OnDestroy, OnInit} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {filter, from, map, Subscription, take, takeLast, tap} from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements  OnInit, OnDestroy {
  title = 'RxJSOperators';
  subCustomer4!: Subscription;
  
  public ngOnInit() {
  
    const customer$ = from([
							 { id: 1, jerseyNumber: 23, age: 61, fullName: "Michael Jordan"},
							 { id: 2, jerseyNumber: 23, age: 43, fullName: "Lebron James"},
							 { id: 3, jerseyNumber: 24, age: 89, fullName: "Kobe Bryant"}
						   ]);
						   
    this.subCustomer4 = customer$.
    pipe(
        filter( item => item.jerseyNumber === 23),
        take(1)
        ).
    subscribe((result) => { console.log(result); });

  }
  
  public ngOnDestroy() {
  
    this.subCustomer4.unsubscribe();
  }
}

The filter(item => item.jerseyNumber === 23), filters the data to only include customers with a jerseyNumber of 23.

This will allow only Michael Jordan and LeBron James through the stream.

While take(1) operator limits the stream to emit only the first item that passes the filter condition.

After emitting this first item, it automatically completes the Observable.

Let’s see the output below.

Observable

RxJS takeLast Operator

The takeLast function emits only the last specified number of values after the source observable completes.

Let us see an example.

import {Component, OnDestroy, OnInit} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {filter, from, map, Subscription, take, takeLast, tap} from 'rxjs';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements  OnInit, OnDestroy {
  title = 'RxJSOperators';
  subCustomer5!: Subscription;

  public ngOnInit() {
  
    const customer$ = from([
							 { id: 1, jerseyNumber: 23, age: 61, fullName: "Michael Jordan"},
							 { id: 2, jerseyNumber: 23, age: 43, fullName: "Lebron James"},
							 { id: 3, jerseyNumber: 24, age: 89, fullName: "Kobe Bryant"}
						   ]);
															 
    this.subCustomer5 = customer$.
      pipe(takeLast(1)).
      subscribe((result) => { console.log(result); });

  }
  public ngOnDestroy() {
  
    this.subCustomer5.unsubscribe();
  }
}

The take last operator is used to emit only the last item from the Observable.

In this example, takeLast(1) will only emit the very last item in the list—{ id: 3, jersey number: 24, age: 89, fullName: "Kobe Bryant" }—and then complete the Observable.

Last item

Summary

In this post, we have discussed RxJS operators and shown the types of operators, such as pipeable and creation operators.

We have seen examples of map, tap, filter, take, and takeLast operators.

I hope you have enjoyed this article as much as I have enjoyed writing it.

Stay tuned for more information, and don't forget to download the attached project source code.

If you run the project source code, don’t forget to command “npm install.”

Until next time, happy programming, and good luck with your career!