Subscribe & Unsubscribe — The Secret to No Memory Leaks
Newsletter Series: RxJS Deep Dive for Angular Developers
Written so clearly, even a 5th grader can understand it.
👋 Welcome Back!
In Chapter 1, we learned that an Observable is like a water pipe — and subscribe() is how you open the tap to receive data.
But here's the thing nobody tells beginners:
If you forget to CLOSE the tap, water keeps flowing forever — even after you leave the room.
In code, that's called a memory leak. 💧 And it's one of the most common bugs in Angular apps.
Today we're going deep into subscribe() — how it works, all its options, and the right way to unsubscribe to keep your app healthy.
🔁 Quick Recap from Chapter 1
// Open the tap — start receiving data
this.userService.getUsers().subscribe((users) => {
this.users = users;
});
Simple. But what happens when the component is destroyed? The subscription is still alive! Like a leaking tap 💧
🧠 Understanding Subscriptions — The Full Picture
When you call .subscribe(), it returns a Subscription object.
import { Subscription } from 'rxjs';
const subscription: Subscription = someObservable.subscribe(value => {
console.log(value);
});
This subscription object is your remote control for the data stream. With it, you can:
- Check if it's still active:
subscription.closed - Stop receiving data:
subscription.unsubscribe()
🍿 Story Time: The Never-Ending Movie
Imagine you went to a cinema 🎬 and started watching a movie. Then you got up and left — but the cinema kept charging you for the seat even though you're gone!
That's a memory leak. The movie (Observable) is still playing (running), and you're still paying (using memory), even though you left (component destroyed).
Unsubscribing is like telling the cinema: "I'm leaving, stop charging me."
🔴 The Problem — Memory Leak in Action
Here's a BAD example that causes a memory leak:
// ❌ BAD — causes memory leak!
@Component({
selector: 'app-dashboard',
template: `<p>Dashboard loaded!</p>`
})
export class DashboardComponent implements OnInit {
constructor(private dataService: DataService) {}
ngOnInit(): void {
// This subscription is NEVER cleaned up!
// Even when the component is destroyed, this keeps running.
this.dataService.getLiveData().subscribe(data => {
console.log('New data:', data);
});
}
}
When the user navigates away from this component, Angular destroys it. But the subscription to getLiveData() is still alive, still calling the callback, still using memory. If the user navigates back and forth 10 times, there are now 10 subscriptions all running at once! 😱
✅ Fix #1 — Manual Unsubscribe
The most basic way: store the subscription and unsubscribe in ngOnDestroy
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-dashboard',
template: `<p>Dashboard: {{ latestData }}</p>`
})
export class DashboardComponent implements OnInit, OnDestroy {
latestData: any;
// Store the subscription so we can cancel it later
private dataSubscription: Subscription = new Subscription();
constructor(private dataService: DataService) {}
ngOnInit(): void {
// Start the subscription and SAVE it
this.dataSubscription = this.dataService.getLiveData().subscribe(data => {
this.latestData = data;
});
}
ngOnDestroy(): void {
// ✅ Component is being destroyed — CANCEL the subscription!
this.dataSubscription.unsubscribe();
console.log('Subscription cleaned up!');
}
}
✅ Fix #2 — Managing Multiple Subscriptions
What if you have 3 different subscriptions? You can use one Subscription object to manage all of them using .add():
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-dashboard',
template: `...`
})
export class DashboardComponent implements OnInit, OnDestroy {
// One Subscription object to rule them all
private subscriptions = new Subscription();
ngOnInit(): void {
// Add subscription 1
this.subscriptions.add(
this.userService.getUsers().subscribe(users => {
this.users = users;
})
);
// Add subscription 2
this.subscriptions.add(
this.productService.getProducts().subscribe(products => {
this.products = products;
})
);
// Add subscription 3
this.subscriptions.add(
this.notificationService.getAlerts().subscribe(alerts => {
this.alerts = alerts;
})
);
}
ngOnDestroy(): void {
// ✅ One line cancels ALL subscriptions
this.subscriptions.unsubscribe();
}
}
✅ Fix #3 — The Angular Way: async Pipe (Recommended!)
Angular has a magical pipe called async that automatically subscribes AND unsubscribes for you!
No need to manually call .subscribe() or .unsubscribe() at all!
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-users',
template: `
<!-- async pipe handles subscribe AND unsubscribe automatically! -->
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
<!-- Show loading -->
<p *ngIf="(users$ | async) === null">Loading... ⏳</p>
`
})
export class UsersComponent {
// Note the $ suffix — convention for Observables
users$: Observable<User[]>;
constructor(private userService: UserService) {
// Just assign the Observable — don't subscribe!
this.users$ = this.userService.getUsers();
}
// No ngOnDestroy needed! async pipe handles cleanup.
}
💡 Pro Tip: In Angular, variables that hold Observables are usually named with a
$at the end (likeusers$,data$). It's just a convention — it tells other developers "hey, this is an Observable!"
✅ Fix #4 — takeUntil Pattern (Advanced but Powerful)
This is the pattern used in large Angular applications. You create a special "destroy signal" Subject (we'll cover Subject later), and use takeUntil to automatically unsubscribe from everything when the component dies.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-dashboard',
template: `...`
})
export class DashboardComponent implements OnInit, OnDestroy {
// This is our "destroy signal"
private destroy$ = new Subject<void>();
ngOnInit(): void {
// takeUntil(this.destroy$) means:
// "Keep subscribing UNTIL destroy$ emits a value"
this.userService.getUsers()
.pipe(takeUntil(this.destroy$))
.subscribe(users => {
this.users = users;
});
this.productService.getProducts()
.pipe(takeUntil(this.destroy$))
.subscribe(products => {
this.products = products;
});
}
ngOnDestroy(): void {
// 🔔 Fire the destroy signal — ALL subscriptions with takeUntil cancel
this.destroy$.next();
this.destroy$.complete();
}
}
📋 The Full Subscribe Syntax — All Options
.subscribe() can take a full object with three handlers:
someObservable.subscribe({
next: (value) => {
// ✅ Called every time a new value arrives
console.log('Got value:', value);
},
error: (err) => {
// ❌ Called if something goes wrong
console.error('Something broke:', err);
// Note: After an error, the Observable STOPS.
// complete() will NOT be called after error()
},
complete: () => {
// 🏁 Called when the Observable says it's done
// This is NOT called if there was an error
console.log('Stream finished!');
}
});
Or the shorthand (just pass a function for next):
// Short version — only handles next values
someObservable.subscribe(value => {
console.log(value);
});
🛍️ Real World Example — E-commerce Cart Watcher
In a real e-commerce app, you might want to watch the shopping cart for changes:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { CartService } from './cart.service';
@Component({
selector: 'app-cart-icon',
template: `
<div class="cart-icon">
🛒 <span class="badge">{{ cartCount }}</span>
</div>
`
})
export class CartIconComponent implements OnInit, OnDestroy {
cartCount = 0;
private cartSub: Subscription = new Subscription();
constructor(private cartService: CartService) {}
ngOnInit(): void {
// Watch the cart — update count whenever cart changes
this.cartSub = this.cartService.cartItems$.subscribe(items => {
this.cartCount = items.length;
});
}
ngOnDestroy(): void {
// Clean up when header component is removed
this.cartSub.unsubscribe();
}
}
🔍 When Do You NOT Need to Unsubscribe?
Great question! Some Observables complete themselves automatically and don't need unsubscribing:
HTTP Requests — they emit once and complete
// ✅ Safe — HTTP observables complete after one response
this.http.get('/api/users').subscribe(data => { ... });
// No need to unsubscribe — it completes on its own!
Finite Observables — they have a natural end
// ✅ Safe — of() completes immediately
of(1, 2, 3).subscribe(n => console.log(n));
You DO need to unsubscribe from:
-
interval()— ticks forever -
fromEvent()— fires on every click/keypress forever - Custom Observables that never complete
-
BehaviorSubject/Subjectstreams from services
🧪 Full Real-World Component Example
Here's a real-world notification bell component that handles everything properly:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { NotificationService } from './notification.service';
interface Notification {
id: number;
message: string;
isRead: boolean;
}
@Component({
selector: 'app-notification-bell',
template: `
<div class="bell-wrapper">
<button (click)="togglePanel()">
🔔
<span *ngIf="unreadCount > 0" class="badge">{{ unreadCount }}</span>
</button>
<div *ngIf="isPanelOpen" class="notification-panel">
<h3>Notifications</h3>
<div *ngFor="let n of notifications"
[class.unread]="!n.isRead">
{{ n.message }}
</div>
<p *ngIf="notifications.length === 0">No notifications!</p>
</div>
</div>
`
})
export class NotificationBellComponent implements OnInit, OnDestroy {
notifications: Notification[] = [];
unreadCount = 0;
isPanelOpen = false;
private destroy$ = new Subject<void>();
constructor(private notifService: NotificationService) {}
ngOnInit(): void {
// Subscribe to live notifications — auto-cleanup with takeUntil
this.notifService.notifications$
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (notifs) => {
this.notifications = notifs;
this.unreadCount = notifs.filter(n => !n.isRead).length;
},
error: (err) => {
console.error('Notification error:', err);
}
});
}
togglePanel(): void {
this.isPanelOpen = !this.isPanelOpen;
}
ngOnDestroy(): void {
// All subscriptions clean up automatically
this.destroy$.next();
this.destroy$.complete();
}
}
🎯 Which Method Should You Use?
Here's a simple guide:
Use async pipe when you're displaying data directly in the template — it's the cleanest, most Angular-idiomatic way.
Use takeUntil when you have logic inside the subscription that can't go in the template — this is the most popular pattern in large apps.
Use manual Subscription when you need fine-grained control over a specific subscription.
🧠 Chapter 2 Summary — What You Learned
-
.subscribe()returns aSubscriptionobject - Forgetting to unsubscribe = memory leak — subscriptions keep running even after the component is gone
- Use
ngOnDestroy()to clean up subscriptions when a component is destroyed - The
asyncpipe is the easiest way — it handles subscribe AND unsubscribe automatically - The
takeUntil+Subjectpattern is the most popular in large apps - HTTP Observables are safe — they complete themselves after one response
- Interval, fromEvent, and Service streams must be manually unsubscribed
📚 Coming Up in Chapter 3...
You know how to subscribe and unsubscribe. Now it's time to learn about pipe() and operators — the superpowers that make RxJS incredibly powerful!
We'll cover map, filter, tap, and more — with real-world examples.
See you in Chapter 3! 🚀
💌 RxJS Deep Dive Newsletter Series | Chapter 2 of 10
Follow me on : Github Linkedin Threads Youtube Channel
Top comments (0)