RxJS is the library behind HttpClient, forms events and the router. You do not need all of it. A few ideas cover most Angular work.
What you will learn
- What an Observable is and how it differs from a Promise
- The operators
map,filter,debounceTimeandswitchMap - How to build a live search box
Observable in plain words
An Observable is a stream of values over time. You subscribe to receive them. Compare:
- Values: Promise: One; Observable: Zero, one or many
- Starts: Promise: Immediately; Observable: When you subscribe
- Cancel: Promise: No; Observable: Yes, by unsubscribing
// src/app/rxjs-demo.ts
import { of, from, interval } from 'rxjs';
import { map, filter, take } from 'rxjs/operators';
// of() makes an Observable from fixed values
of(1, 2, 3, 4, 5)
.pipe(
filter((n) => n % 2 === 1), // keep 1, 3, 5
map((n) => n * 10), // 10, 30, 50
)
.subscribe((value) => console.log('value', value));
// from() makes an Observable from an array or a Promise
from(Promise.resolve('done')).subscribe(console.log);
// interval() emits every second. take(3) stops it after three values.
interval(1000).pipe(take(3)).subscribe((n) => console.log('tick', n));
pipe() chains operators. Each operator takes a stream and returns a new one.
Common operators
mapchanges each value.filterkeeps only the values that pass a test.debounceTime(300)waits until the stream is quiet for 300 ms. It is perfect for typing.switchMapstarts a new inner request and cancels the previous one. It prevents old slow answers from replacing newer ones.catchErrorhandles an error and returns a fallback.takeUntilDestroyedunsubscribes when the component is destroyed.
A live search box
// src/app/search/search-box.ts
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { catchError, debounceTime, distinctUntilChanged, of, switchMap } from 'rxjs';
interface Post {
id: number;
title: string;
}
@Component({
selector: 'app-search-box',
imports: [ReactiveFormsModule],
template: `
<label for="q">Search posts</label>
<input id="q" type="search" [formControl]="query" />
<ul>
@for (post of results(); track post.id) {
<li>{{ post.title }}</li>
} @empty {
<li>Type at least 2 letters.</li>
}
</ul>
`,
})
export class SearchBox {
private readonly http = inject(HttpClient);
protected readonly query = new FormControl('', { nonNullable: true });
protected readonly results = toSignal(
this.query.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((text) =>
text.trim().length < 2
? of([] as Post[])
: this.http
.get<Post[]>('https://jsonplaceholder.typicode.com/posts', {
params: { q: text.trim() },
})
.pipe(catchError(() => of([] as Post[]))),
),
),
{ initialValue: [] as Post[] },
);
}
// src/app/app.ts
import { Component } from '@angular/core';
import { SearchBox } from './search/search-box';
@Component({
imports: [SearchBox],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<h1>Search</h1>
<app-search-box />
</main>
The key parts
query.valueChangesis an Observable that emits whenever the user types.debounceTime(300)avoids a request for every key press.distinctUntilChanged()skips a value equal to the previous one.switchMapswaps to the new request and cancels the old one.catchErrorinsideswitchMapkeeps the outer stream alive after an error. If you put it outside, one error would stop the search for good.toSignalturns the final stream into a signal.initialValueavoidsundefined.
Signals or Observables?
Use signals for values held in your component or store. Use Observables for streams of events and async work such as HTTP, debounced typing and WebSockets. toSignal and toObservable connect the two worlds.
Try it yourself
- Change the debounce time to 1000 ms and notice the difference.
- Remove
switchMapformergeMapand type quickly. What can go wrong? - Use
mapto show titles in upper case.
Quick check
- When does an Observable start producing values?
- What does
switchMapdo to the previous inner request? - Why put
catchErrorinsideswitchMap?
Next, continue with RxJS Observables Explained: Create from Arrays, Strings, Events with of, from and fromEvent.