Reactive programming is one of the most powerful paradigms in modern frontend development, and Angular makes it possible through RxJS (Reactive Extensions for JavaScript).
Among the many classes provided by RxJS, the BehaviorSubject stands out as one of the most commonly used and essential tools for maintaining and broadcasting state across your Angular application.
This comprehensive guide explores everything about BehaviorSubject — from the basic definition and syntax to advanced real-world use cases, examples, and best practices.
What Is a BehaviorSubject?
In RxJS, a Subject is both an Observable and an Observer. This means it can:
- Emit values to multiple subscribers.
- Receive new values via the
.next()
method.
However, unlike a normal Subject, a BehaviorSubject has one special property:
It always holds the latest emitted value, and new subscribers immediately receive that value upon subscription.
In simpler words, if you subscribe to a BehaviorSubject after it has already emitted some values, you will still get the most recent value instantly.
Syntax of BehaviorSubject
The BehaviorSubject is imported from the rxjs
library.
import { BehaviorSubject } from 'rxjs';
To create one, you must provide an initial value when constructing it:
const mySubject = new BehaviorSubject<number>(0);
Now, mySubject
is both an observable and an observer.
Basic Example of BehaviorSubject
Here’s the simplest demonstration:
import { BehaviorSubject } from 'rxjs';
const count$ = new BehaviorSubject<number>(0);
count$.subscribe(value => console.log('Subscriber 1:', value));
count$.next(1);
count$.next(2);
count$.subscribe(value => console.log('Subscriber 2:', value));
count$.next(3);
Output:
Subscriber 1: 0
Subscriber 1: 1
Subscriber 1: 2
Subscriber 2: 2
Subscriber 1: 3
Subscriber 2: 3
Explanation:
- Subscriber 1 subscribes immediately and receives the initial value
0
. - Subscriber 2 subscribes later, after
2
has been emitted, but still immediately receives the latest value (2). - When
.next(3)
is called, both subscribers receive the new value.
This behavior distinguishes BehaviorSubject from a regular Subject, which would not emit the last value to new subscribers.
BehaviorSubject vs Subject
Feature | Subject | BehaviorSubject |
---|---|---|
Requires Initial Value | No | Yes |
Stores Latest Value | No | Yes |
Emits Last Value to New Subscribers | No | Yes |
Typical Use Case | Event streams | Application state, shared data |
Example: Regular Subject
import { Subject } from 'rxjs';
const subject = new Subject<number>();
subject.next(1);
subject.subscribe(value => console.log('Subscriber:', value));
subject.next(2);
Output:
Subscriber: 2
Notice that 1
was not received by the subscriber because it subscribed after the value was emitted.
BehaviorSubject, on the other hand, would immediately send the latest value upon subscription.
Accessing the Current Value
You can get the current value stored inside a BehaviorSubject at any time using the .value
property.
const counter$ = new BehaviorSubject<number>(0);
counter$.next(5);
console.log(counter$.value);
Output:
5
This is useful when you need to read the state synchronously without subscribing.
Updating Values with .next()
To emit new data, use the .next()
method.
const message$ = new BehaviorSubject<string>('Hello');
message$.subscribe(msg => console.log('Received:', msg));
message$.next('Hi there');
message$.next('Welcome to RxJS!');
Output:
Received: Hello
Received: Hi there
Received: Welcome to RxJS!
Each .next()
call broadcasts the new value to all active subscribers.
BehaviorSubject in Angular Applications
In Angular, BehaviorSubject is most often used inside services to store and share application state between components.
This makes it perfect for:
- Managing global state (like user authentication, theme, or settings).
- Sharing data between parent and child components.
- Caching API responses.
- Implementing reactive forms or component communication.
Example: Sharing Data Between Components
Let’s create a simple example where we share a value between two components using BehaviorSubject.
Step 1: Create a Service
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CounterService {
private countSubject = new BehaviorSubject<number>(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
decrement() {
this.countSubject.next(this.countSubject.value - 1);
}
reset() {
this.countSubject.next(0);
}
}
Step 2: Component A (Incrementer)
import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-incrementer',
template: `
<button (click)="increase()">Increment</button>
`
})
export class IncrementerComponent {
constructor(private counterService: CounterService) {}
increase() {
this.counterService.increment();
}
}
Step 3: Component B (Display)
import { Component, OnInit } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-display',
template: `
<p>Current Count: {{ count }}</p>
`
})
export class DisplayComponent implements OnInit {
count = 0;
constructor(private counterService: CounterService) {}
ngOnInit() {
this.counterService.count$.subscribe(value => {
this.count = value;
});
}
}
How It Works
CounterService
uses a BehaviorSubject to hold and broadcast the count.IncrementerComponent
updates the count using.next()
through service methods.DisplayComponent
subscribes tocount$
and reacts to every change immediately.
Even if a component subscribes later, it will still receive the latest count value instantly.
Using .asObservable()
Notice that in the service, we exposed count$
as an Observable using .asObservable()
:
count$ = this.countSubject.asObservable();
This is an important best practice.
It ensures that components can subscribe to the BehaviorSubject but cannot directly modify its value using .next()
.
Only the service itself can call .next()
internally.
BehaviorSubject in State Management
BehaviorSubject is frequently used as a lightweight state management solution when you don’t want to use libraries like NgRx or Akita.
For example, you can store and reactively update global state like user authentication or theme preference.
Example: Auth State Management
@Injectable({
providedIn: 'root'
})
export class AuthService {
private userSubject = new BehaviorSubject<string | null>(null);
user$ = this.userSubject.asObservable();
login(username: string) {
this.userSubject.next(username);
}
logout() {
this.userSubject.next(null);
}
get currentUser() {
return this.userSubject.value;
}
}
Component Example
@Component({
selector: 'app-header',
template: `
<div *ngIf="user; else guest">
Welcome, {{ user }}
</div>
<ng-template #guest>
<p>Please login.</p>
</ng-template>
`
})
export class HeaderComponent implements OnInit {
user: string | null = null;
constructor(private authService: AuthService) {}
ngOnInit() {
this.authService.user$.subscribe(value => this.user = value);
}
}
Now, the header automatically updates when a user logs in or out, thanks to the reactive stream of BehaviorSubject.
Late Subscribers and Immediate Emission
One of the key features of BehaviorSubject is that new subscribers immediately get the most recent value, even if they subscribe much later.
Example:
const status$ = new BehaviorSubject<string>('Offline');
status$.next('Online');
status$.subscribe(value => console.log('New subscriber got:', value));
Output:
New subscriber got: Online
The new subscriber instantly received the last known value (Online
) instead of waiting for the next .next()
call.
Using BehaviorSubject with Async Pipe
Instead of manually subscribing and unsubscribing, you can use Angular’s AsyncPipe in templates.
Example
Service:
@Injectable({
providedIn: 'root'
})
export class DataService {
private messageSubject = new BehaviorSubject<string>('Hello');
message$ = this.messageSubject.asObservable();
updateMessage(newMsg: string) {
this.messageSubject.next(newMsg);
}
}
Component Template:
<p>{{ dataService.message$ | async }}</p>
<button (click)="update()">Change Message</button>
Component Class:
constructor(public dataService: DataService) {}
update() {
this.dataService.updateMessage('Updated Message!');
}
The AsyncPipe automatically subscribes to the observable and displays the latest value. When the component is destroyed, AsyncPipe also handles the unsubscription automatically, avoiding memory leaks.
BehaviorSubject and ReplaySubject Differences
Another RxJS class similar to BehaviorSubject is ReplaySubject.
Here’s how they differ:
Feature | BehaviorSubject | ReplaySubject |
---|---|---|
Stores latest value only | Yes | Can store multiple previous values |
Emits last value to new subscribers | Yes | Emits a specified number of previous values |
Requires initial value | Yes | No |
Typical use case | Application state | Replaying event history |
Example with ReplaySubject:
import { ReplaySubject } from 'rxjs';
const replay$ = new ReplaySubject<number>(2); // stores last 2 values
replay$.next(1);
replay$.next(2);
replay$.next(3);
replay$.subscribe(value => console.log('Subscriber:', value));
Output:
Subscriber: 2
Subscriber: 3
ReplaySubject replays the last 2 values to new subscribers.
Combining BehaviorSubjects with Operators
BehaviorSubjects work seamlessly with RxJS operators like map
, filter
, distinctUntilChanged
, and combineLatest
.
Example 1: Derived Stream
const price$ = new BehaviorSubject<number>(100);
const discounted$ = price$.pipe(
map(price => price * 0.9)
);
discounted$.subscribe(value => console.log('Discounted Price:', value));
When price$
emits a new value, discounted$
automatically recalculates and emits the new discounted value.
Example 2: Combining Multiple BehaviorSubjects
import { combineLatest, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
const firstName$ = new BehaviorSubject<string>('John');
const lastName$ = new BehaviorSubject<string>('Doe');
const fullName$ = combineLatest([firstName$, lastName$]).pipe(
map(([first, last]) => ${first} ${last}
)
);
fullName$.subscribe(name => console.log('Full Name:', name));
Output:
Full Name: John Doe
Changing either firstName$
or lastName$
automatically updates fullName$
.
BehaviorSubject with HTTP Requests
You can use BehaviorSubject to cache API results and share them across multiple components.
Example: API Data Caching
@Injectable({
providedIn: 'root'
})
export class UserService {
private usersSubject = new BehaviorSubject<any[]>([]);
users$ = this.usersSubject.asObservable();
constructor(private http: HttpClient) {}
loadUsers() {
this.http.get<any[]>('/api/users').subscribe(users => {
this.usersSubject.next(users);
});
}
}
Component A:
ngOnInit() {
this.userService.loadUsers();
}
Component B:
ngOnInit() {
this.userService.users$.subscribe(users => console.log(users));
}
Both components now share the same user data without making redundant HTTP requests.
Unsubscribing from BehaviorSubject
When manually subscribing (not using AsyncPipe), always unsubscribe in ngOnDestroy
to prevent memory leaks.
subscription!: Subscription;
ngOnInit() {
this.subscription = this.service.count$.subscribe();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
Best Practices
- Expose BehaviorSubjects as Observables
- Use
.asObservable()
to prevent external components from mutating the subject.
- Use
- Keep BehaviorSubject in Services
- Use it to manage shared or global state, not directly in components.
- Avoid Storing Complex Mutable Objects
- Always emit new copies instead of mutating the existing object.
const current = this.userSubject.value; this.userSubject.next({ ...current, name: 'Updated Name' });
- Use AsyncPipe Whenever Possible
- Reduces boilerplate and handles subscriptions automatically.
- Don’t Overuse BehaviorSubjects
- For large-scale applications, consider using NgRx or other dedicated state management solutions.
Testing BehaviorSubject
Testing is straightforward since BehaviorSubject provides synchronous value emission.
import { BehaviorSubject } from 'rxjs';
describe('BehaviorSubject', () => {
it('should emit initial and next values', () => {
const bs = new BehaviorSubject<number>(0);
const results: number[] = [];
bs.subscribe(val => results.push(val));
bs.next(1);
bs.next(2);
expect(results).toEqual([0, 1, 2]);
});
});
Common Mistakes
- Forgetting to provide an initial value.
BehaviorSubject always requires one. - Mutating objects directly.
This prevents proper change detection. - Overusing
.value
property.
It’s fine for reads, but updates should go through.next()
. - Subscribing without unsubscribing.
Always unsubscribe manually or use AsyncPipe.
Leave a Reply