Using BehaviorSubject in Angular and RxJS

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:

  1. Emit values to multiple subscribers.
  2. 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

FeatureSubjectBehaviorSubject
Requires Initial ValueNoYes
Stores Latest ValueNoYes
Emits Last Value to New SubscribersNoYes
Typical Use CaseEvent streamsApplication 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: `
&lt;button (click)="increase()"&gt;Increment&lt;/button&gt;
` }) 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: `
&lt;p&gt;Current Count: {{ count }}&lt;/p&gt;
` }) export class DisplayComponent implements OnInit { count = 0; constructor(private counterService: CounterService) {} ngOnInit() {
this.counterService.count$.subscribe(value =&gt; {
  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 to count$ 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: `
&lt;div *ngIf="user; else guest"&gt;
  Welcome, {{ user }}
&lt;/div&gt;
&lt;ng-template #guest&gt;
  &lt;p&gt;Please login.&lt;/p&gt;
&lt;/ng-template&gt;
` }) export class HeaderComponent implements OnInit { user: string | null = null; constructor(private authService: AuthService) {} ngOnInit() {
this.authService.user$.subscribe(value =&gt; 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:

FeatureBehaviorSubjectReplaySubject
Stores latest value onlyYesCan store multiple previous values
Emits last value to new subscribersYesEmits a specified number of previous values
Requires initial valueYesNo
Typical use caseApplication stateReplaying 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&lt;any&#91;]&gt;('/api/users').subscribe(users =&gt; {
  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

  1. Expose BehaviorSubjects as Observables
    • Use .asObservable() to prevent external components from mutating the subject.
  2. Keep BehaviorSubject in Services
    • Use it to manage shared or global state, not directly in components.
  3. 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' });
  4. Use AsyncPipe Whenever Possible
    • Reduces boilerplate and handles subscriptions automatically.
  5. 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&lt;number&gt;(0);
const results: number&#91;] = &#91;];
bs.subscribe(val =&gt; results.push(val));
bs.next(1);
bs.next(2);
expect(results).toEqual(&#91;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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *