Skip to content
Angular ng rxjs 4 min read

Introduction to RxJS

RxJS (Reactive Extensions for JavaScript) is the library Angular uses to model values that arrive over time — HTTP responses, router events, form changes, WebSocket messages, and user interactions. Instead of imagining a value as a single thing you fetch once, reactive programming treats it as a stream you can listen to, transform, combine, and react to. If you understand observables, you understand a large slice of how Angular actually works under the hood.

What is reactive programming?

Reactive programming is a style of writing code that reacts to changes rather than pulling values on demand. You declare how data should flow and what should happen when new values appear, and the library pushes those values to you as they happen.

The central abstraction is the Observable: a lazy producer of zero, one, or many values over time. An observable does nothing until someone subscribes to it. Each value it emits is delivered to your callback, and the observable can also signal that it has completed (no more values) or errored (something went wrong).

ConceptMeaning
ObservableA source that produces values over time (the stream)
ObserverThe set of callbacks (next, error, complete) that consume values
SubscriptionThe live connection created by subscribe(); can be torn down
OperatorA pure function (map, filter, switchMap, …) that transforms a stream
EmissionA single value pushed out of the observable

Your first observable

You create an observable, then subscribe to start receiving its emissions. The Observer can be a single function (the next handler) or an object with next, error, and complete.

import { Observable } from 'rxjs';

const numbers$ = new Observable<number>((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

numbers$.subscribe({
  next: (value) => console.log('Got value:', value),
  error: (err) => console.error('Error:', err),
  complete: () => console.log('Stream complete'),
});

Output:

Got value: 1
Got value: 2
Got value: 3
Stream complete

The trailing $ on numbers$ is a widely used convention signalling “this variable is an observable.” It is not syntax — just a readability hint that keeps streams visually distinct from plain values.

Why Angular leans on RxJS

Angular exposes many of its asynchronous APIs as observables, so reactive programming is not optional knowledge — it is the language Angular speaks. The most common source is HttpClient, whose methods all return observables.

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs';

interface User {
  id: number;
  name: string;
}

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      <ul>
        @for (user of users; track user.id) {
          <li>{{ user.name }}</li>
        }
      </ul>
    } @else {
      <p>Loading users…</p>
    }
  `,
})
export class UserListComponent {
  private http = inject(HttpClient);
  users$: Observable<User[]> = this.http.get<User[]>('/api/users');
}

The async pipe subscribes to users$ for you, renders each emission, and automatically unsubscribes when the component is destroyed — eliminating an entire class of memory leaks. The new @if/@for control flow reads the stream’s latest value declaratively.

Operators: transforming streams

The real power of RxJS is in operators — small, composable functions you chain inside pipe() to reshape a stream. Operators are pure: they return a new observable and never mutate the source.

import { from } from 'rxjs';
import { filter, map } from 'rxjs';

const result$ = from([1, 2, 3, 4, 5, 6]).pipe(
  filter((n) => n % 2 === 0),
  map((n) => n * 10),
);

result$.subscribe((value) => console.log(value));

Output:

20
40
60

Here from turns an array into a stream, filter lets only even numbers through, and map multiplies each survivor by ten. Because each operator returns a fresh observable, you can read the pipeline top-to-bottom like a data-processing recipe.

Observables are lazy and (usually) cold

Two properties trip up newcomers. First, observables are lazy: the function you pass to new Observable(...) does not run until subscribe() is called. Second, most observables are cold — each subscriber triggers its own independent execution.

import { defer } from 'rxjs';

const random$ = defer(() => {
  const value = Math.random();
  return [value];
});

random$.subscribe((v) => console.log('A:', v));
random$.subscribe((v) => console.log('B:', v));

Output:

A: 0.4172
B: 0.8391

Each subscription re-ran the producer, so A and B see different random numbers. When you instead want shared execution that multicasts one result to many subscribers, you reach for Subjects or the share() operator.

Best practices

  • Suffix observable variables with $ so streams are instantly recognizable in code review.
  • Prefer the async pipe in templates over manual subscribe() — it handles subscription lifecycle for you and prevents leaks.
  • Keep transformation logic inside pipe() with pure operators rather than doing work inside subscribe callbacks.
  • Remember observables are lazy: nothing runs until subscription, so side effects belong in tap or the subscriber, not in setup code.
  • Always have a teardown strategy for manual subscriptions (the async pipe, takeUntilDestroyed, or storing the Subscription).
  • Reach for share()/shareReplay() or a Subject when multiple consumers should share one execution instead of triggering it repeatedly.
Last updated June 14, 2026
Was this helpful?