AngularJSSubjects & BehaviorSubject

Subjects & BehaviorSubject in Angular

A Subject is both an Observable (you can subscribe to it) and an Observer (you can push values into it with .next()). This dual nature makes Subjects the go-to tool for broadcasting values to multiple subscribers and for building lightweight shared state in Angular services.

Subject Types at a Glance

Type

Replays on Subscribe

Initial Value

Best For

Subject

Nothing (only future values)

None

Event bus, one-way broadcasts

BehaviorSubject

Last emitted value

Required

Current state (user, theme, cart)

ReplaySubject(N)

Last N values

None

Caching recent values for late subscribers

AsyncSubject

Last value on complete

None

One final result (rarely used)

Subject — Basic Usage

TS
import { Subject } from 'rxjs';

const subject = new Subject<string>();

// Subscriber A — subscribes before any value
subject.subscribe(v => console.log('A:', v));

subject.next('hello'); // A: hello
subject.next('world'); // A: world

// Subscriber B — subscribes after previous values, misses them
subject.subscribe(v => console.log('B:', v));

subject.next('!'); // A: ! and B: !
A: hello
A: world
A: !
B: !
Note
A plain Subject does not replay past values. Late subscribers only see future emissions.
BehaviorSubject — Stateful Value

BehaviorSubject stores the current value and replays it to every new subscriber. This makes it perfect for representing current application state.

TS
import { BehaviorSubject } from 'rxjs';

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

count$.subscribe(v => console.log('Subscriber 1:', v)); // immediately: 0

count$.next(1); // Subscriber 1: 1
count$.next(2); // Subscriber 1: 2

// Late subscriber gets the CURRENT value immediately
count$.subscribe(v => console.log('Subscriber 2:', v)); // immediately: 2

count$.next(3); // Subscriber 1: 3, Subscriber 2: 3

// Read current value synchronously (no subscription needed)
console.log('Current value:', count$.getValue()); // 3
Subscriber 1: 0
Subscriber 1: 1
Subscriber 1: 2
Subscriber 2: 2
Subscriber 1: 3
Subscriber 2: 3
Current value: 3
Using BehaviorSubject in an Angular Service

This is the most common Angular pattern for lightweight shared state — a service that holds a BehaviorSubject and exposes it as a read-only Observable.

TS
// src/app/services/cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  // Private — only the service can push new values
  private cartItems$ = new BehaviorSubject<CartItem[]>([]);

  // Public read-only Observable — components subscribe to this
  readonly items$: Observable<CartItem[]> = this.cartItems$.asObservable();

  // Derived Observables from the same source
  readonly count$: Observable<number> = this.items$.pipe(
    map(items => items.reduce((sum, item) => sum + item.quantity, 0))
  );

  readonly total$: Observable<number> = this.items$.pipe(
    map(items => items.reduce((sum, item) => sum + item.price * item.quantity, 0))
  );

  addItem(newItem: CartItem): void {
    const current = this.cartItems$.getValue();
    const existing = current.find(i => i.id === newItem.id);

    if (existing) {
      const updated = current.map(i =>
        i.id === newItem.id ? { ...i, quantity: i.quantity + 1 } : i
      );
      this.cartItems$.next(updated);
    } else {
      this.cartItems$.next([...current, { ...newItem, quantity: 1 }]);
    }
  }

  removeItem(id: number): void {
    const filtered = this.cartItems$.getValue().filter(i => i.id !== id);
    this.cartItems$.next(filtered);
  }

  clearCart(): void {
    this.cartItems$.next([]);
  }
}
Tip
Expose the Subject as .asObservable() to prevent external code from calling .next() directly on it — keep mutations inside the service.
Cart Component Using the Service

TS
// src/app/components/cart/cart.component.ts
import { Component, inject } from '@angular/core';
import { AsyncPipe, CurrencyPipe } from '@angular/common';
import { CartService } from '../../services/cart.service';

@Component({
  selector: 'app-cart',
  standalone: true,
  imports: [AsyncPipe, CurrencyPipe],
  template: `
    <h2>Cart ({{ cartService.count$ | async }} items)</h2>

    @for (item of cartService.items$ | async; track item.id) {
      <div class="cart-item">
        <span>{{ item.name }} x{{ item.quantity }}</span>
        <span>{{ item.price * item.quantity | currency }}</span>
        <button (click)="cartService.removeItem(item.id)">Remove</button>
      </div>
    }

    <strong>Total: {{ cartService.total$ | async | currency }}</strong>
    <button (click)="cartService.clearCart()">Clear</button>
  `,
})
export class CartComponent {
  cartService = inject(CartService);
}
ReplaySubject — Cache N Values for Late Subscribers

TS
import { ReplaySubject } from 'rxjs';

// Buffer last 3 values
const replay$ = new ReplaySubject<number>(3);

replay$.next(1);
replay$.next(2);
replay$.next(3);
replay$.next(4);

// Late subscriber receives 2, 3, 4 (the last 3)
replay$.subscribe(v => console.log('Late subscriber:', v));
Late subscriber: 2
Late subscriber: 3
Late subscriber: 4

Common use case: caching recent log messages or notifications so a component that mounts late still sees recent events.

Using Subjects as an Event Bus

A plain Subject works perfectly as an inter-component event bus when you need to communicate between components that don't share a parent.

TS
// src/app/services/notification.service.ts
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';

export interface Notification {
  type: 'success' | 'error' | 'info';
  message: string;
}

@Injectable({ providedIn: 'root' })
export class NotificationService {
  private notification$ = new Subject<Notification>();

  // Any component can subscribe to incoming notifications
  readonly notifications$: Observable<Notification> = this.notification$.asObservable();

  success(message: string): void {
    this.notification$.next({ type: 'success', message });
  }

  error(message: string): void {
    this.notification$.next({ type: 'error', message });
  }

  info(message: string): void {
    this.notification$.next({ type: 'info', message });
  }
}

// Usage in any component
@Injectable()
export class SomeComponent {
  private notify = inject(NotificationService);

  save(): void {
    this.http.post('/api/save', this.data).subscribe({
      next: () => this.notify.success('Saved successfully!'),
      error: () => this.notify.error('Save failed.'),
    });
  }
}
Subject as a takeUntil Trigger

One of the most common Subject patterns in Angular is using a plain Subject to signal component destruction.

TS
import { Component, OnDestroy } from '@angular/core';
import { Subject, interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({ selector: 'app-timer', standalone: true, template: `{{ count }}` })
export class TimerComponent implements OnDestroy {
  count = 0;
  private destroy$ = new Subject<void>();

  constructor() {
    interval(1000)
      .pipe(takeUntil(this.destroy$)) // unsubscribe when destroy$ emits
      .subscribe(() => this.count++);
  }

  ngOnDestroy(): void {
    this.destroy$.next();    // signal completion
    this.destroy$.complete(); // close the Subject
  }
}
Note
In Angular 16+, prefer takeUntilDestroyed() from @angular/core/rxjs-interop — it hooks into the Angular destroy lifecycle automatically without needing a manual Subject.
BehaviorSubject vs Signal — When to Use Which

BehaviorSubject

Signal

Syntax

subject.next(value)

signal.set(value)

Read value

subject.getValue()

signal()

Async operators

Full RxJS pipe support

Limited (use toObservable)

Template

Needs async pipe or toSignal

Direct binding

Best for

Complex async flows, HTTP streams

Simple component / service state

Best Practices
  • Expose Subjects as asObservable() to prevent external code from calling .next()

  • Use BehaviorSubject when components need the current value on subscription

  • Use plain Subject for event buses and takeUntil teardown

  • Use ReplaySubject(N) when late subscribers need recent history

  • Always call subject.complete() in ngOnDestroy to release resources

  • For Angular 16+, consider signals for simple state; keep BehaviorSubject for complex RxJS pipelines

  • Derive computed values with .pipe(map(...)) rather than duplicating state

Summary

Subjects bridge the imperative and reactive worlds — you push values in imperatively and subscribers react reactively. BehaviorSubject is the workhorse for Angular service-level state management: it holds current state, replays it to new subscribers, and lets you derive computed streams with RxJS operators. For simple cases in Angular 16+, signals offer a lighter alternative; for complex async flows, BehaviorSubject remains the right tool.