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
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: !
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.
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()); // 3Subscriber 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.
// 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([]);
}
}.asObservable() to prevent external code from calling .next() directly on it — keep mutations inside the service.Cart Component Using the Service
// 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
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.
// 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.
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
}
}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.