RxJS Basics for Angular: Observables and Operators

Angular Tutorial: Beginner to Advanced · 10. HTTP and RxJS · 4 min read · Updated

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, debounceTime and switchMap
  • 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

  • map changes each value.
  • filter keeps only the values that pass a test.
  • debounceTime(300) waits until the stream is quiet for 300 ms. It is perfect for typing.
  • switchMap starts a new inner request and cancels the previous one. It prevents old slow answers from replacing newer ones.
  • catchError handles an error and returns a fallback.
  • takeUntilDestroyed unsubscribes when the component is destroyed.
// 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.valueChanges is 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.
  • switchMap swaps to the new request and cancels the old one.
  • catchError inside switchMap keeps the outer stream alive after an error. If you put it outside, one error would stop the search for good.
  • toSignal turns the final stream into a signal. initialValue avoids undefined.

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

  1. Change the debounce time to 1000 ms and notice the difference.
  2. Remove switchMap for mergeMap and type quickly. What can go wrong?
  3. Use map to show titles in upper case.

Quick check

  1. When does an Observable start producing values?
  2. What does switchMap do to the previous inner request?
  3. Why put catchError inside switchMap?

Next, continue with RxJS Observables Explained: Create from Arrays, Strings, Events with of, from and fromEvent.

Frequently asked questions

Do I need to learn every RxJS operator?

No. map, filter, switchMap, debounceTime, catchError and take cover most daily work. Look up others when you need them.

What is the dollar sign at the end of names like users$?

It is a naming habit meaning the value is an Observable. It is optional.