Skip to content
Angular ng rxjs 4 min read

Subjects & BehaviorSubject

A plain observable is unicast: every subscriber triggers its producer function from scratch and gets its own private execution. A Subject breaks that rule — it is both an observable and an observer, so you can push values into it imperatively with next() while many subscribers share a single stream. That dual nature makes the Subject family the backbone of event buses, component-to-component messaging, and the classic observable data service pattern in Angular.

Subject: the multicast primitive

A Subject keeps a list of subscribers and forwards every next, error, and complete to all of them at once. Crucially, it does not replay anything — a subscriber only sees values emitted after it subscribes.

import { Subject } from 'rxjs';

const events$ = new Subject<string>();

events$.subscribe((v) => console.log('A:', v));
events$.next('first');

events$.subscribe((v) => console.log('B:', v));
events$.next('second');

Output:

A: first
A: second
B: second

Subscriber B missed 'first' because it joined late. This is the defining behaviour of a bare Subject.

BehaviorSubject: always has a current value

BehaviorSubject requires an initial value and remembers the latest value it emitted. Any new subscriber immediately receives that current value, then continues to get subsequent emissions. You can also read the value synchronously via .value. This makes it ideal for state — a piece of data that always has a “now.”

import { BehaviorSubject } from 'rxjs';

const count$ = new BehaviorSubject<number>(0);

count$.subscribe((v) => console.log('A:', v)); // gets 0 right away
count$.next(1);

count$.subscribe((v) => console.log('B:', v)); // gets current value 1
count$.next(2);

console.log('current =', count$.value);

Output:

A: 0
A: 1
B: 1
A: 2
B: 2
current = 2

ReplaySubject and AsyncSubject

ReplaySubject buffers a configurable number of past values (optionally within a time window) and replays them to every new subscriber. AsyncSubject emits only the last value, and only when the source complete()s — useful for one-shot results.

import { ReplaySubject, AsyncSubject } from 'rxjs';

const replay$ = new ReplaySubject<number>(2); // keep last 2
replay$.next(1);
replay$.next(2);
replay$.next(3);
replay$.subscribe((v) => console.log('replay:', v)); // gets 2, 3

const async$ = new AsyncSubject<number>();
async$.next(10);
async$.next(20);
async$.subscribe((v) => console.log('async:', v));
async$.complete(); // emits only 20

Output:

replay: 2
replay: 3
async: 20

Choosing the right variant

TypeInitial valueReplays to late subscribersTypical use
SubjectNoNothingEvent bus, notifications, action streams
BehaviorSubjectRequiredLatest valueShared state, current selection, auth status
ReplaySubjectNoLast n (or time window)Caching recent events, audit logs
AsyncSubjectNoFinal value on completeOne-shot async result

The observable data service pattern

The most common Angular use is to keep a BehaviorSubject private inside a service and expose only its read-only observable. Consumers subscribe to the public stream and mutate state exclusively through methods — they can never call next() directly.

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

export interface CartItem {
  id: string;
  name: string;
  qty: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = new BehaviorSubject<CartItem[]>([]);

  /** Public read-only stream — components subscribe to this. */
  readonly items$ = this.items.asObservable();

  add(item: CartItem): void {
    this.items.next([...this.items.value, item]);
  }

  clear(): void {
    this.items.next([]);
  }
}

In a standalone component you can render the stream with the async pipe, or bridge it to a signal with toSignal for ergonomic template access:

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CartService } from './cart.service';

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    @if (items().length) {
      <ul>
        @for (item of items(); track item.id) {
          <li>{{ item.name }} ×{{ item.qty }}</li>
        }
      </ul>
    } @else {
      <p>Your cart is empty.</p>
    }
  `,
})
export class CartComponent {
  private readonly cart = inject(CartService);
  readonly items = toSignal(this.cart.items$, { initialValue: [] });
}

Tip: .asObservable() hides the next/error/complete methods at the type level, enforcing one-way data flow. Exposing the raw Subject lets any consumer push values and quietly destroys your single source of truth.

Warning: A Subject is hot and stateful. Once you call error() or complete() on it, it is dead — every future subscriber instantly receives that terminal notification and can never get new values. For long-lived app state, never complete the subject early.

Best practices

  • Default to BehaviorSubject for shared state so every subscriber always gets a meaningful current value, and reach for plain Subject only for fire-and-forget events.
  • Keep the subject private and expose asObservable() (or a toSignal view); mutate state only through service methods.
  • Treat each next() as an immutable update — emit new arrays/objects ([...prev, item]) rather than mutating in place so change detection and equality checks work.
  • Prefer the async pipe or toSignal in templates over manual .subscribe() so subscriptions clean up automatically.
  • Use ReplaySubject(1) instead of BehaviorSubject when you genuinely have no sensible initial value but still want late subscribers to receive the most recent emission.
  • Avoid calling complete() on app-wide state subjects; let services live for the app’s lifetime and reset state by emitting a new value.
Last updated June 14, 2026
Was this helpful?