Key RxJS Operators Every Angular Developer Should Know

Top RxJS Operators I Frequently Use Examples Included

ยท

5 min read

Play this article

Today, one of our teammates was curious about which RxJS operators they'd need to create a search feature in Angular. I let them know that while RxJS has loads of operators, they don't need to know them all. I usually stick with Map, Filter, Tap, SwitchMap, ConcatMap, CombineLatest, StartWith, DistinctUntilChanged, DebounceTime, and CatchError.

So instead of overwhelming our teammate with every marble diagram, I thought showing them a practical example would be more helpful. I used the "dummyjson API" to create a search feature and included all the essential RxJS operators I mentioned earlier. This way, they could see how everything works together in a real-life scenario. ๐Ÿ˜Š

We must create a product search feature for the Product Search page, which fetches data from the "dummyjson API." Users can search for products by name, and the app will display the results.

We have two main actors search.component.ts and search.service.ts.

The Search Component

  • The Search component has an HTML input; we listen to the event (input) every time the user types a keystroke.

  • The event is managed by the searchByTerm method invokes the search service with each keystroke.

  • We utilize the async pipe to obtain data from the products$ observable and the data emitted by this.searchService.products$.

import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchService } from './search.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'search',
  standalone: true,
  providers: [SearchService],
  imports: [CommonModule],
  template: `
    <div>
      <h1>Search</h1>
      <input type="text" #searchTerm (input)="searchByTerm(searchTerm.value)" />
      <div class="product" *ngIf="products$ | async as products">
        <div *ngFor="let product of products">
          <h3>{{ product.title }}</h3>
          <img [alt]="product.title" [src]="product.thumbnail" />
        </div>
      </div>
    </div>
  `,
})
export class SearchComponent {
  #searchService = inject(SearchService);
  products$: Observable<any> = this.#searchService.products$;

  searchByTerm(value: string) {
    this.#searchService.searchByText(value);
  }
}

๐Ÿ‘‰๐Ÿฝ We could enhance this process by using fromEvent and piping the data, but for now, we'll focus on showcasing some popular operators.

The Search Service

This is the crux of the article: We use RxJS operators in combination with HttpClient retrieving data and making API requests, and we pipe the data with the operators. However, the communication is made possible thanks to a special guest: the Observable Subject.

Let's take a moment to explain the Observable Subject, as some readers may have never used it or might be unfamiliar with it.

The Subject is a type Observable that enables both emitting data and subscribing to it.

 #searchTermSubject = new Subject<string>()

When the user types in the component, it triggers the searchByText function in the service. We then utilize the searchTermSubject to emit the data using the .next() method.

Next, we create the observable products$ from searchTermSubject emissions and apply all the RxJS operators to it.

products$ = this.searchTermSubject.asObservable().pipe(
//the rxjsoperators
)

Now we covered the idea about the Subject, so let us work with the operators in the stream of data from searchTermSubject.

We aim to incorporate the following features into the search:

  • Prevent excessive API requests when the user types rapidly.

  • Avoid making duplicate requests to the API.

  • Skip searches with fewer than three letters.

  • Fetch data from the API only after the previous conditions have been met.

  • Extract a specific property from the API response.

The final code looks like this:

import {inject, Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {catchError, debounceTime, distinctUntilChanged, filter, map, of, Subject, switchMap, tap} from "rxjs";

@Injectable()
export class SearchService {

  http = inject(HttpClient);
  #API ='https://dummyjson.com/products/search'

  #searchTermSubject = new Subject<string>()

  products$ = this.searchTermSubject.asObservable().pipe(
    debounceTime(500),
    distinctUntilChanged(),
    filter(termSearch => termSearch.trim().length > 5),
    tap((searchTerm) => console.log(`The search term: ${searchTerm}`)),
    switchMap((searchTerm) => this.fetchProducts(searchTerm)),
    map((response: any) => response.products)
  );

  searchByText(term: string) {
    this.searchTermSubject.next(term.trim());
  }

  private fetchProducts(searchTerm: string) {
    const apiUrl = `${this.#API}?q=${searchTerm}`;
    return this.http.get(apiUrl).pipe(
      catchError((error) => {
        console.error('Error getting Products', error)
        return of([])
      })
    );
  }
}

Hey there! Let's take a moment to go through these operators step by step, so you can understand what each one does. ๐Ÿ˜Š

The RxJS Operators

  1. DebounceTime: Reducing Rapid Events The debounceTime operator limits the rate at which events are processed. By using debounceTime(500), we ensure that our service only processes user input every 500 milliseconds, preventing excessive API requests when the user types rapidly.

     debounceTime(500),
    
  2. DistinctUntilChanged: Eliminating Duplicates The distinctUntilChanged operator ensures that consecutive duplicate values are not emitted. This helps to avoid making duplicate requests to the API.

     distinctUntilChanged(),
    
  3. Filter: Refining the Search The filter operator refines our query by ensuring it is at least three characters long before proceeding to the API. This narrows down the search results.

     filter(q => q.length > 2),
    
  4. SwitchMap: Handling Observables, The switchMap operator allows us to switch between observables. In this example, we use switchMap to fetch data from the API only after the user input has passed through debounceTime, distinctUntilChanged, and filter.

     switchMap(q => this.http.get(`${this.apiURL}?q=${q}`).pipe(/* ... */)),
    
  5. CatchError: Handling Errors The catchError operator helps us handle errors gracefully if the API call encounters any issues.

     catchError(error => /* handle error */),
    
  6. Map: Transforming Data The map operator transforms the data emitted by an observable. In our example, we use it to extract the product property from the API response.

     map((response: any) => response.products),
    
  7. Tap: Side Effects The tap operator allows us to perform side effects, such as logging, without affecting the main data flow. In our case, we use it to log the fetched products.

  tap((products) => console.log('Fetched products:', products))

It's finished; we've learned each operator's purpose and explanations for their use in our search service.

Conclusion

We've used all the operators we discussed earlier, and I hope they made sense to you. These are the ones I use quite often. There's another popular operator called combineLatest that we didn't include here, but don't worry. You can check it out in another article.

To summarize, combined operators are super handy when working with RxJS. Even though there are many operators, the ones we discussed should help you with your everyday tasks. I really hope these examples make your RxJS journey a bit easier!

If like please share!๐Ÿ˜Š

ย