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. 😊
Building The Search
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 to retrieve data and make 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 of 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
)
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([])
})
);
}
}
Ok, but what are all these operators doing? let's explain one by one.
The RxJS Operators
-
DebounceTime: Reducing Rapid Events The
debounceTime
operator limits the rate at which events are processed. By usingdebounceTime(500)
, we ensure that our service only processes user input every 500 milliseconds, preventing excessive API requests when the user types rapidly.
debounceTime(500),
-
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(),
-
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),
-
SwitchMap: Handling Observables, The
switchMap
operator allows us to switch between observables. In this example, we useswitchMap
to fetch data from the API only after the user input has passed throughdebounceTime
,distinctUntilChanged
, andfilter
.
switchMap(q => this.http.get(`${this.apiURL}?q=${q}`).pipe(/* ... */)),
-
CatchError: Handling Errors The
catchError
operator helps us handle errors gracefully if the API call encounters any issues.
catchError(error => /* handle error */),
-
Map: Transforming Data The
map
operator transforms the data emitted by an observable. In our example, we use it to extract theproduct
property from the API response.
map((response: any) => response.products),
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!😊
Top comments (2)
Great post! Something worth sharing with Angular starters
Question: I understand the use of "$" as observables. What is the reason behind of using "octothorp (#)" in variable declaring?
The
#
is a syntax for private methods :)developer.mozilla.org/en-US/docs/W...