RxJS Observables in Angular
The observables Angular uses under the hood for events, HTTP, and async data, and the handful of operators you'll use daily.
阅读需 2 分钟
Before signals existed, Angular relied heavily on RxJS, a library for working with asynchronous streams of values called observables. Angular still uses observables extensively — the router, HttpClient, and reactive forms all return them — so reading and using one is an essential skill even in an app that also uses signals.
What makes an observable different from a promise
A promise represents a single future value. An observable represents a stream of values over time — zero, one, or many — and nothing happens until something subscribes to it:
import { interval } from 'rxjs';
const ticks$ = interval(1000);
const subscription = ticks$.subscribe((tick) => {
console.log('Tick:', tick);
});
// later, to stop listening:
subscription.unsubscribe();The trailing $ on ticks$ is a naming convention (not a language feature) that signals "this is an observable" at a glance. Nothing runs until .subscribe() is called — an unsubscribed observable is inert, which is different from a promise, whose executor function starts running the moment it's created.
Transforming a stream with operators
RxJS operators let you transform, filter, or combine values as they flow through, using .pipe():
import { fromEvent } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';
const searchInput = document.querySelector('input')!;
fromEvent<InputEvent>(searchInput, 'input')
.pipe(
map((event) => (event.target as HTMLInputElement).value),
filter((value) => value.length > 2),
debounceTime(300),
)
.subscribe((value) => {
console.log('Searching for:', value);
});map transforms each emitted value, filter drops values that don't match a condition, and debounceTime(300) waits for 300ms of silence before letting a value through — a common pattern for search inputs, so a request doesn't fire on every keystroke. Operators compose left to right inside .pipe(), each one receiving the previous operator's output.
The async pipe: letting the template handle subscriptions
Manually subscribing inside a component means remembering to unsubscribe in ngOnDestroy, every time, or risking a memory leak. Angular's async pipe subscribes for you and unsubscribes automatically when the component is destroyed:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-article-list',
template: `
@for (article of articles$ | async; track article.id) {
<p>{{ article.title }}</p>
}
`,
})
export class ArticleListComponent {
private http = inject(HttpClient);
articles$ = this.http.get<{ id: number; title: string }[]>('/api/articles');
}articles$ | async subscribes to the observable when the template renders and unwraps each emitted value directly into articles-shaped data the @for block can iterate — no manual subscription, no manual cleanup, and no risk of forgetting either.
Signals and observables side by side
Angular doesn't ask you to choose one exclusively. HttpClient and the router still return observables, since a stream models one-off HTTP responses and ongoing router events well. Signals tend to fit component-local, synchronous state better. Angular even provides toSignal() to convert an observable into a signal when you want the rest of a component to read it that way — you'll see this combination in the HTTP client lesson later in this course.