Angular 42 🅰️ Subjects, BehaviorSubject, and ReplaySubject
An Observable is a stream that a consumer subscribes to, and the producer is the Observable’s own subscriber function. A Subject is different: it is both an Observable and an Observer. It can be subscribed to like an Observable, and it can emit values like an Observer. This dual nature is what makes it the bridge between imperative code — a button click, a service method — and reactive code — a pipeline, a template binding. Angular applications use Subjects for cross-component communication, for state that is written imperatively and read reactively, and for the multicast pattern where one producer feeds many consumers. The four variants — Subject, BehaviorSubject, ReplaySubject, and AsyncSubject — differ in what they replay to a new subscriber, and the choice among them is the design decision that determines whether a late subscriber sees the current state, the whole history, or nothing at all. This chapter covers each variant, the patterns they enable, and the pitfalls that make them a common source of bugs.
Key point: A plain Subject is hot and stateless — it emits to whatever subscribers exist at the moment of emission, and a subscriber that arrives late misses everything that came before. A BehaviorSubject requires an initial value and replays the most recent value to every new subscriber, which makes it the natural choice for state that has a current value. A ReplaySubject replays a configurable number of previous values, which is useful for history and for late subscribers that need context. An AsyncSubject emits only the last value, and only when it completes, which is rarely used in Angular. Subjects are the imperative escape hatch in a reactive system, and the discipline is to expose them as Observables to consumers so the emission path is controlled.
What a Subject is
A Subject is a multicast Observable. A plain Observable creates a new producer for each subscriber — a cold stream. A Subject has a single producer and delivers its values to all current subscribers — a hot stream.
import { Subject } from 'rxjs';
const subject = new Subject<number>();
subject.subscribe((v) => console.log('A:', v));
subject.subscribe((v) => console.log('B:', v));
subject.next(1);
subject.next(2);
// A: 1
// B: 1
// A: 2
// B: 2
Both subscribers receive both values. The next method emits to all subscribers at once, which is the multicast behavior.
Why a Subject is both Observable and Observer. A Subject implements both interfaces. As an Observable, it has subscribe. As an Observer, it has next, error, and complete. This is what lets it sit between imperative code that calls next and reactive code that subscribes.
Why a Subject is hot. The producer is the code that calls next, not the subscription. The values are emitted whether or not anyone is subscribed, and a subscriber that arrives after an emission misses it. This is the defining characteristic and the source of the most common surprise.
Why a Subject is used as an event bus. An event bus is a pattern where components publish events to a shared channel and other components subscribe. The Subject is the channel. A component calls next to publish, and the subscribers receive the event. The pattern is common in Angular for cross-component communication, and the Subject is the primitive that makes it work.
Why a Subject is a state container. When the value of the Subject is the current state — the current user, the current filter, the current theme — the Subject holds that value, and subscribers react to changes. This is the pattern that BehaviorSubject formalizes by holding the current value and replaying it.
Why the late-subscriber problem matters. In an Angular component, a subscription often happens in
ngOnInitor in a constructor, which may run after the value has been emitted. With a plain Subject, the late subscriber misses the value and the component renders with no data. TheBehaviorSubjectandReplaySubjectexist to solve this: they replay the value so the late subscriber sees it.
Subject — the plain multicast
A plain Subject is the simplest variant. It emits to all current subscribers and holds nothing.
private readonly destroy$ = new Subject<void>();
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
someObservable$.pipe(
takeUntil(this.destroy$),
).subscribe();
The destroy$ Subject is the standard pre-Angular-16 pattern for unsubscribing. Each subscription pipes through takeUntil(this.destroy$), and ngOnDestroy calls next and complete to terminate all of them. This is a common use of a plain Subject: it is a signal, not a state, and the value it emits is irrelevant.
Why the value is void for a destroy Subject. The Subject is a signal that something happened — the component is being destroyed. The value is not used. Typing it as void makes the intent clear.
Why complete is called after next. Calling next notifies the takeUntil operators, which complete their subscriptions. Calling complete completes the Subject itself, which releases its subscribers. Both are good practice, and the order matters: the notification first, then the completion.
Why a plain Subject is right for events. An event has no current state. A button click, a “save” command, a “refresh” signal — these are events, and a subscriber that arrives after the event should not receive it. The plain Subject matches this semantics exactly.
Why a plain Subject is wrong for state. A subscriber that arrives late needs the current state, not nothing. A plain Subject does not hold the state, so the late subscriber sees nothing. For state, a BehaviorSubject is the right tool.
BehaviorSubject — the current value
A BehaviorSubject requires an initial value and replays the most recent value to every new subscriber. It is the standard choice for state in an Angular service.
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly currentUserSubject = new BehaviorSubject<User | null>(null);
readonly currentUser$ = this.currentUserSubject.asObservable();
login(user: User): void {
this.currentUserSubject.next(user);
}
logout(): void {
this.currentUserSubject.next(null);
}
}
The currentUserSubject holds the current user, starting as null. The currentUser$ is the public Observable that consumers subscribe to. When the user logs in or out, next is called, and every subscriber receives the new value. A subscriber that arrives late immediately receives the current value.
Why the initial value is required. A BehaviorSubject always has a value — the current value — so it must be initialized. null is the standard initial value when there is no current user, no current filter, or no current selection. The initial value is delivered to every new subscriber before any subsequent emissions.
Why the getter pattern is used. The service exposes currentUser$ as an Observable, and the Subject is private. This is the discipline that prevents components from calling next on the Subject, which would make the state mutable from outside the service. The service is the only writer, and the components are readers.
Why the .value property is sometimes used. BehaviorSubject has a synchronous .value property that returns the current value. It is useful when the current value is needed without subscribing, but it is an escape from the reactive model and should be used sparingly. The reactive way is to subscribe to the Observable and react to changes.
Why asObservable() is the standard export. The asObservable() method returns an Observable view of the Subject. The Subject still has next, but the returned Observable does not, so consumers cannot emit. This is the mechanism that enforces the write-only-from-service discipline.
Why BehaviorSubject is not the same as a signal. A signal is a reactive value that tracks its dependencies and is read synchronously. A BehaviorSubject is a stream that emits asynchronously, and consumers subscribe. Both hold a current value, but the model is different. Angular is moving toward signals for state, and BehaviorSubject remains the RxJS primitive for the same role.
Why the initial value is a design decision. The initial value is the state before anything has happened.
nullfor “no user,”[]for “no items,”'light'for “default theme.” The choice affects every subscriber, because each receives the initial value on subscription. Getting it wrong — an empty array that a component interprets as “loaded with no results” — is a common source of confusion.
ReplaySubject — the history
A ReplaySubject replays a configurable number of previous values to each new subscriber. The number and the time window are both configurable.
const replay = new ReplaySubject<number>(2);
replay.next(1);
replay.next(2);
replay.next(3);
replay.subscribe((v) => console.log(v));
// 2
// 3
The ReplaySubject(2) holds the last two values. The late subscriber receives 2 and 3 on subscription, then any subsequent values.
Why replay is useful. A subscriber that needs context — the last few events, the recent history of a value — needs the previous values. A BehaviorSubject gives only the most recent, and a ReplaySubject gives the last N.
The time-window variant. ReplaySubject also accepts a time argument, which limits the replay to values emitted within the given window.
const recent = new ReplaySubject<number>(undefined, 5000);
The 5000 is the time window in milliseconds. Only values emitted in the last five seconds are replayed. This is useful when the history has a time relevance and older values are stale.
Why ReplaySubject can be a memory concern. The Subject holds the last N values in memory. A large N with large values is a memory cost. The buffer is bounded, so it does not grow indefinitely, but the buffer size is a decision that should be made deliberately.
Why ReplaySubject is less common than BehaviorSubject. Most state has a current value and no history requirement. The BehaviorSubject covers the current-value case, and the history case is rarer. When history is needed — a log, a recent-activity list, an undo buffer — the ReplaySubject is the tool.
Why ReplaySubject(1) is like a BehaviorSubject but not the same. A ReplaySubject(1) replays the last value, like a BehaviorSubject. The differences: the ReplaySubject does not require an initial value, so a subscriber before the first next receives nothing; and the BehaviorSubject has the synchronous .value property. The two are similar but not interchangeable.
AsyncSubject — the final value
An AsyncSubject emits only the last value, and only when it completes. It is rarely used in Angular, but it has a specific niche.
const async = new AsyncSubject<number>();
async.subscribe((v) => console.log(v));
async.next(1);
async.next(2);
async.next(3);
async.complete();
// 3
The subscriber receives nothing until complete is called, and then receives only the last value. All previous values are ignored.
Why AsyncSubject exists. It models an operation that produces a single result at completion — a computation that emits progress but only the final value matters. In practice, a Promise or a firstValueFrom covers most of these cases, and the AsyncSubject is rare.
Why it is rarely used in Angular. Angular’s HTTP client returns a cold Observable that emits once and completes. There is no need for a Subject in the HTTP path. The AsyncSubject appears in code that models a single-result stream, which is uncommon.
Why it is worth knowing. The four Subject variants are a family, and the AsyncSubject completes the family. Knowing it exists prevents the mistake of reaching for a BehaviorSubject when the value should only be emitted at completion.
Subjects as the imperative bridge
The main use of Subjects in Angular is to bridge imperative code and reactive code. A component method, an event handler, a service call — these are imperative. A pipeline, a template binding, a reactive computation — these are reactive. The Subject is the point where the imperative code writes and the reactive code reads.
The service-as-state pattern. A service holds a BehaviorSubject for the state and exposes it as an Observable. Methods on the service call next to change the state. Components subscribe to the Observable to react to changes.
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly itemsSubject = new BehaviorSubject<CartItem[]>([]);
readonly items$ = this.itemsSubject.asObservable();
add(item: CartItem): void {
this.itemsSubject.next([...this.itemsSubject.value, item]);
}
remove(id: string): void {
this.itemsSubject.next(
this.itemsSubject.value.filter((i) => i.id !== id),
);
}
}
The CartService holds the cart state. The add and remove methods are imperative — they take a value and update the state. The items$ is reactive — components subscribe and receive the current items and any updates. The Subject is the bridge.
Why the state is immutable in the update. The add method creates a new array with the spread operator instead of pushing into the existing one. This is the immutable update pattern, and it matters for change detection and for subscribers that compare references.
Why the methods are the only writers. The Subject is private, and the only way to change the state is through the service’s methods. This is the encapsulation that prevents components from mutating the state directly, which would make the state’s changes untraceable.
Why the $ suffix is a convention. The trailing $ on items$ marks it as an Observable. The convention makes the type visible in the name and is standard in Angular code.
Why the Subject should be completed. A long-lived Subject that is never completed holds its subscribers in memory. For a service that lives for the application’s lifetime, this is acceptable. For a component-scoped Subject, completing it in ngOnDestroy releases the subscribers.
Why Subjects are the escape hatch, not the default. A Subject is a mutable variable that happens to be observable. It is the right tool when state must be written imperatively, but it is not the only tool. Signals, cold Observables from HTTP, and
combineLatestof other streams are alternatives that avoid the mutation. The Subject is the tool for the case where the write is imperative, and the discipline is to keep it private and expose only the Observable.
Common patterns
Several patterns appear repeatedly in Angular code, and each is a specific use of a Subject variant.
The destroy signal. A plain Subject used with takeUntil to unsubscribe all subscriptions in a component. Superseded by takeUntilDestroyed in Angular 16+, but still common in existing code.
private readonly destroy$ = new Subject<void>();
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
The state container. A BehaviorSubject in a service, exposed as an Observable, with methods that call next.
The event bus. A plain Subject in a service, with methods that call next and components that subscribe.
@Injectable({ providedIn: 'root' })
export class NotificationService {
private readonly notificationsSubject = new Subject<string>();
readonly notifications$ = this.notificationsSubject.asObservable();
notify(message: string): void {
this.notificationsSubject.next(message);
}
}
The action stream. A Subject that receives user actions and a pipeline that processes them, which is the Redux-like pattern.
private readonly actions$ = new Subject<Action>();
readonly state$ = this.actions$.pipe(
scan((state, action) => this.reducer(state, action), initialState),
shareReplay(1),
);
dispatch(action: Action): void {
this.actions$.next(action);
}
The scan accumulates the state, and the shareReplay shares the latest state with all subscribers. The dispatch method is the imperative write, and the state$ is the reactive read.
The dialog result. A Subject that emits the result of a dialog when the user closes it, and a pipeline that reacts to the result. This is a common pattern for imperative flows like confirmations.
Why these patterns recur. Each is a case where an imperative action must be observed reactively. The Subject is the only primitive that bridges the two, so it appears wherever the boundary is crossed.
Pitfalls
Subjects have failure modes that are worth naming, because they cause bugs that are hard to diagnose.
The late subscriber with a plain Subject. A component subscribes after the value has been emitted and receives nothing. The fix is a BehaviorSubject or a ReplaySubject.
The Subject that is not completed. A long-lived Subject holds its subscribers in memory. For a service, this is usually fine. For a component-scoped Subject, the subscription should be completed.
The Subject that is exposed. A Subject exposed as a public field allows any consumer to call next, which makes the state mutable from outside. The fix is asObservable().
The Subject used for state with no initial value. A BehaviorSubject with null as the initial value is correct; a plain Subject used for state is not, because the late subscriber sees nothing.
The Subject that re-emits on every change. A BehaviorSubject emits the initial value on every subscription, which means a component that subscribes and unsubscribes frequently receives the same value each time. This is usually correct, but it can cause duplicate work if the subscriber does not account for it.
The Subject used where a signal would be better. Angular signals are the modern replacement for the state-container pattern. A signal is synchronous, does not need to be subscribed to, and integrates with change detection. For new code that only needs a current value, a signal is often simpler than a BehaviorSubject.
Why the pitfalls recur. Subjects are mutable state in a reactive system, and mutable state is the source of most bugs. The discipline — private Subject, public Observable, completed when scoped to a component, replaced by signals when the state is simple — is what keeps the pattern healthy.
Why
BehaviorSubjectis being replaced by signals in new Angular code. A signal is a reactive value that tracks its dependencies and is read synchronously. It does not need a subscription and does not have a late-subscriber problem, because reading a signal returns the current value. For the state-container pattern, a signal is simpler and more efficient.BehaviorSubjectremains the right choice when the state must be combined with other RxJS streams or when the project is on an older version of Angular.
Complete Example Session
import { Injectable, Component, inject, DestroyRef } from '@angular/core';
import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject, Observable } from 'rxjs';
import { scan, shareReplay, takeUntil, map } from 'rxjs/operators';
// ============================================
// PART 1: PLAIN SUBJECT
// ============================================
const events$ = new Subject<string>();
events$.subscribe((e) => console.log('A:', e));
events$.next('hello');
events$.subscribe((e) => console.log('B:', e));
events$.next('world');
// A: hello
// A: world
// B: world
// (B missed 'hello')
// ============================================
// PART 2: BEHAVIORSUBJECT
// ============================================
const state$ = new BehaviorSubject<number>(0);
state$.subscribe((v) => console.log('A:', v));
// A: 0 (initial value)
state$.next(1);
// A: 1
state$.subscribe((v) => console.log('B:', v));
// B: 1 (current value replayed)
state$.next(2);
// A: 2
// B: 2
// ============================================
// PART 3: REPLAYSUBJECT
// ============================================
const history$ = new ReplaySubject<number>(2);
history$.next(1);
history$.next(2);
history$.next(3);
history$.subscribe((v) => console.log(v));
// 2
// 3
// ============================================
// PART 4: ASYNCSUBJECT
// ============================================
const result$ = new AsyncSubject<number>();
result$.subscribe((v) => console.log('result:', v));
result$.next(1);
result$.next(2);
result$.next(3);
result$.complete();
// result: 3
// ============================================
// PART 5: STATE SERVICE WITH BEHAVIORSUBJECT
// ============================================
interface CartItem {
id: string;
name: string;
}
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly itemsSubject = new BehaviorSubject<CartItem[]>([]);
readonly items$ = this.itemsSubject.asObservable();
add(item: CartItem): void {
this.itemsSubject.next([...this.itemsSubject.value, item]);
}
remove(id: string): void {
this.itemsSubject.next(
this.itemsSubject.value.filter((i) => i.id !== id),
);
}
clear(): void {
this.itemsSubject.next([]);
}
}
// ============================================
// PART 6: COMPONENT SUBSCRIBING TO STATE
// ============================================
@Component({
selector: 'app-cart',
standalone: true,
template: `
@for (item of cart.items$ | async; track item.id) {
<div>{{ item.name }}</div>
}
`,
})
export class CartComponent {
readonly cart = inject(CartService);
}
// ============================================
// PART 7: DESTROY SIGNAL
// ============================================
@Component({ selector: 'app-old', standalone: true, template: `` })
export class OldStyleComponent {
private readonly destroy$ = new Subject<void>();
ngOnInit(): void {
// someObservable$.pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
// ============================================
// PART 8: REDUX-STYLE ACTION STREAM
// ============================================
type Action =
| { type: 'add'; item: CartItem }
| { type: 'remove'; id: string }
| { type: 'clear' };
interface State {
items: CartItem[];
}
const initialState: State = { items: [] };
@Injectable({ providedIn: 'root' })
export class StoreService {
private readonly actions$ = new Subject<Action>();
readonly state$: Observable<State> = this.actions$.pipe(
scan((state, action) => {
switch (action.type) {
case 'add':
return { items: [...state.items, action.item] };
case 'remove':
return { items: state.items.filter((i) => i.id !== action.id) };
case 'clear':
return { items: [] };
}
}, initialState),
shareReplay({ bufferSize: 1, refCount: true }),
);
dispatch(action: Action): void {
this.actions$.next(action);
}
}
// ============================================
// PART 9: EVENT BUS
// ============================================
@Injectable({ providedIn: 'root' })
export class NotificationService {
private readonly notificationsSubject = new Subject<string>();
readonly notifications$ = this.notificationsSubject.asObservable();
notify(message: string): void {
this.notificationsSubject.next(message);
}
}
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't expose the Subject directly
// readonly items$ = this.itemsSubject; // consumers can call next
// Don't use a plain Subject for state
// A late subscriber misses the current value.
// Don't forget to complete component-scoped Subjects
// The subscriptions leak.
// Don't read .value in a loop
// It bypasses the reactive model.
// Don't use a Subject when a signal would do
// For simple current-value state, signals are simpler.
The ten parts cover each variant, the state service, the component subscription, the destroy signal, the action stream, the event bus, and the anti-patterns.
Quick Reference
The Four Variants
| Subject | Replay | Initial | Use |
|---|---|---|---|
Subject | None | No | Events, destroy signal |
BehaviorSubject | Latest | Required | State with a current value |
ReplaySubject | Last N | No | History, context for late subscribers |
AsyncSubject | Last on complete | No | Single result at completion |
API
| Method | Purpose |
|---|---|
next(value) | Emit |
error(err) | Terminate with error |
complete() | Terminate normally |
subscribe(fn) | Subscribe |
asObservable() | Observable view (no next) |
.value | Current value (BehaviorSubject only) |
Service Pattern
| Step | Code |
|---|---|
| Private subject | private readonly s = new BehaviorSubject<T>(init) |
| Public observable | readonly s$ = this.s.asObservable() |
| Write method | set(v: T) { this.s.next(v); } |
| Read | s$.subscribe(...) or s$ | async |
Subject vs Observable
| Aspect | Observable | Subject |
|---|---|---|
| Producer | Per subscriber | Shared |
| Hot/cold | Cold | Hot |
| Emission | From subscriber fn | From next |
| Late subscriber | Gets all | Gets nothing (plain) |
Subject vs Signal
| Aspect | Subject | Signal |
|---|---|---|
| Read | Subscribe | Synchronous |
| Late subscriber | Depends on variant | Always current |
| Change detection | Manual | Integrated |
| Combination | RxJS operators | Computed |
Best Practices
✅ Do This:
// Keep the Subject private, expose asObservable
private readonly s = new BehaviorSubject<T>(init);
readonly s$ = this.s.asObservable(); // ✅
// Use BehaviorSubject for state with a current value
private readonly userSubject = new BehaviorSubject<User | null>(null); // ✅
// Use plain Subject for events
private readonly notifySubject = new Subject<string>(); // ✅
// Use ReplaySubject for history
const history$ = new ReplaySubject<Event>(10); // ✅
// Update state immutably
this.s.next([...this.s.value, item]); // ✅
// Complete component-scoped Subjects
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } // ✅
// Use takeUntilDestroyed in new code
.pipe(takeUntilDestroyed(this.destroyRef)) // ✅
// Consider a signal for simple current-value state
readonly count = signal(0); // ✅
❌ Don’t Do This:
// Don't expose the Subject directly
readonly items$ = this.itemsSubject; // consumers can next // ⚠️
// Don't use a plain Subject for state
private readonly state = new Subject<State>(); // late misses // ⚠️
// Don't forget to complete component-scoped Subjects
// The subscriptions leak // ⚠️
// Don't read .value in a loop
while (this.s.value) { ... } // bypasses the reactive model // ⚠️
// Don't use a BehaviorSubject with a wrong initial value
new BehaviorSubject<string>(''); // empty string is not "none" // ⚠️
// Don't use a Subject when a signal would do
// Signals are simpler for synchronous state // ⚠️
// Don't mutate the value held by the Subject
this.s.value.push(item); // mutation, not a new emission // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Plain Subject for state | Late subscriber misses value | BehaviorSubject |
| Exposed Subject | External next | asObservable() |
| Uncompleted component Subject | Leak | Complete in ngOnDestroy |
Reading .value in loops | Bypasses reactive | Subscribe |
| Mutating the held value | No emission | Emit a new value |
| Wrong initial value | Wrong first emission | Choose deliberately |
| ReplaySubject with large N | Memory | Bound the buffer |
| Subject where signal fits | Overkill | Use a signal |
Real-World Examples
1. Auth state
private readonly userSubject = new BehaviorSubject<User | null>(null);
readonly user$ = this.userSubject.asObservable();
2. Cart state
private readonly itemsSubject = new BehaviorSubject<CartItem[]>([]);
readonly items$ = this.itemsSubject.asObservable();
3. Notification bus
private readonly notifySubject = new Subject<string>();
readonly notifications$ = this.notifySubject.asObservable();
4. Destroy signal
private readonly destroy$ = new Subject<void>();
5. Redux-style store
readonly state$ = this.actions$.pipe(
scan((s, a) => reducer(s, a), initial),
shareReplay(1),
);
6. Recent history
const recent$ = new ReplaySubject<Event>(5);
7. Dialog result
private readonly resultSubject = new Subject<boolean>();
8. Form value stream
form.valueChanges.pipe(switchMap(...));
9. Theme state
private readonly themeSubject = new BehaviorSubject<'light' | 'dark'>('light');
10. Combined state
readonly vm$ = combineLatest([this.user$, this.items$]).pipe(
map(([user, items]) => ({ user, items })),
);
Visual: Subject Variants
┌──────────────────────────────────────────────────────────┐
│ Subject │
│ next(1) next(2) next(3) │
│ ─────────────────────────────► │
│ │
│ Subscriber A (from start): 1, 2, 3 │
│ Subscriber B (after 2): 3 │
│ B missed 1 and 2. │
│ │
├──────────────────────────────────────────────────────────┤
│ BehaviorSubject(initial = 0) │
│ next(1) next(2) next(3) │
│ ─────────────────────────────► │
│ │
│ Subscriber A (from start): 0, 1, 2, 3 │
│ Subscriber B (after 2): 2, 3 │
│ B received the current value 2. │
│ │
├──────────────────────────────────────────────────────────┤
│ ReplaySubject(N = 2) │
│ next(1) next(2) next(3) │
│ ─────────────────────────────► │
│ │
│ Subscriber A (from start): 1, 2, 3 │
│ Subscriber B (after 3): 2, 3 │
│ B received the last 2. │
│ │
├──────────────────────────────────────────────────────────┤
│ AsyncSubject │
│ next(1) next(2) next(3) complete() │
│ ───────────────────────────────────► │
│ │
│ Subscriber A (from start): 3 (on complete) │
│ Subscriber B (before complete): 3 │
│ Only the last value, only on complete. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The State Service Pattern
┌──────────────────────────────────────────────────────────┐
│ SERVICE │
│ │
│ private readonly s = new BehaviorSubject<T>(init); │
│ readonly s$ = this.s.asObservable(); │
│ │
│ set(v: T) { this.s.next(v); } │
│ │ │
│ │ the only writer │
│ ▼ │
│ ┌─────────────┐ │
│ │ Behavior │ │
│ │ Subject │ │
│ └──────┬──────┘ │
│ │ │
│ ├──► component A (s$ | async) │
│ ├──► component B (subscribe) │
│ └──► component C (combineLatest) │
│ │
│ Components read; the service writes. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Late Subscriber Problem
┌──────────────────────────────────────────────────────────┐
│ PLAIN SUBJECT │
│ │
│ t=0 next(user) │
│ t=1 component subscribes │
│ t=2 component receives nothing │
│ (the value was emitted before subscription) │
│ │
│ Component renders with no data. │
│ │
├──────────────────────────────────────────────────────────┤
│ BEHAVIORSUBJECT │
│ │
│ t=0 next(user) │
│ t=1 component subscribes │
│ t=2 component receives user immediately │
│ (the current value is replayed) │
│ │
│ Component renders with the current data. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Redux-Style Store
┌──────────────────────────────────────────────────────────┐
│ dispatch(action) │
│ │ │
│ ▼ │
│ actions$: Subject<Action> │
│ │ │
│ ▼ │
│ scan(reducer, initialState) │
│ │ accumulates state │
│ ▼ │
│ state$: Observable<State> │
│ │ │
│ ├──► shareReplay(1) │
│ │ shares the latest with all subscribers │
│ │ │
│ ├──► component A │
│ ├──► component B │
│ └──► component C │
│ │
│ One dispatch, one state, many readers. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Subject vs Signal
┌──────────────────────────────────────────────────────────┐
│ BehaviorSubject │
│ │
│ private readonly s = new BehaviorSubject(0); │
│ readonly s$ = this.s.asObservable(); │
│ │
│ Read: s$.subscribe(v => ...) │
│ Write: this.s.next(1); │
│ Late: receives current value │
│ │
├──────────────────────────────────────────────────────────┤
│ Signal │
│ │
│ readonly count = signal(0); │
│ │
│ Read: count() │
│ Write: count.set(1) or count.update(v => v + 1) │
│ Late: always current, synchronous read │
│ │
│ Signals do not need a subscription. │
│ They integrate with change detection. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Subject | Replay | Initial | Complete | Use |
|---|---|---|---|---|
Subject | None | No | Required for cleanup | Events, destroy |
BehaviorSubject | Latest | Required | No | State |
ReplaySubject | Last N | No | No | History |
AsyncSubject | Last on complete | No | Required | Rare |
| Pattern | Subject | Purpose |
|---|---|---|
| State service | BehaviorSubject | Current state |
| Event bus | Subject | Cross-component events |
| Destroy signal | Subject | Unsubscribe |
| Action stream | Subject + scan | Redux-style store |
| History | ReplaySubject | Recent context |
Key takeaways:
- A Subject is both an Observable and an Observer — it can be subscribed to and it can emit, which makes it the bridge between imperative and reactive code
- A plain
Subjectis hot and stateless — late subscribers miss earlier emissions, which makes it right for events and wrong for state - A
BehaviorSubjectholds a current value and replays it — it is the standard choice for state, and the initial value is a design decision - A
ReplaySubjectreplays the last N values — it is for history and for late subscribers that need context, and the buffer is a bounded memory cost - An
AsyncSubjectemits only the last value on completion — it is rarely used in Angular because HTTP and promises cover its cases - The service pattern keeps the Subject private and exposes
asObservable()— this is what makes the service the only writer and the components the readers - The destroy signal is the pre-Angular-16 cleanup pattern — a plain Subject with
takeUntil, superseded bytakeUntilDestroyed - The Redux-style store is a
Subjectof actions piped throughscan— theshareReplayshares the latest state with all subscribers - Signals are the modern replacement for the state container — a signal is synchronous, needs no subscription, and integrates with change detection
- The pitfalls all come from mutable state — the private Subject, the public Observable, the completed scope, and the immutable update are the discipline that keeps the pattern healthy
Remember: Subjects are the imperative bridge in a reactive system. A plain Subject is for events, a BehaviorSubject is for state, a ReplaySubject is for history, and an AsyncSubject is for the rare single-result case. Keep the Subject private, expose the Observable, complete the scope, and consider a signal when the state is simple. The discipline is what separates a Subject that helps from a Subject that causes the bugs it was supposed to prevent.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!