DEV Community

Cover image for RxJS in Angular — Chapter 2 | Subscribe & Unsubscribe — The Secret to No Memory Leaks
Jack Pritom Soren
Jack Pritom Soren

Posted on

RxJS in Angular — Chapter 2 | Subscribe & Unsubscribe — The Secret to No Memory Leaks

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;
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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!');
  }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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.
}
Enter fullscreen mode Exit fullscreen mode

💡 Pro Tip: In Angular, variables that hold Observables are usually named with a $ at the end (like users$, 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

📋 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!');
  }
});
Enter fullscreen mode Exit fullscreen mode

Or the shorthand (just pass a function for next):

// Short version — only handles next values
someObservable.subscribe(value => {
  console.log(value);
});
Enter fullscreen mode Exit fullscreen mode

🛍️ 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

🔍 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!
Enter fullscreen mode Exit fullscreen mode

Finite Observables — they have a natural end

// ✅ Safe — of() completes immediately
of(1, 2, 3).subscribe(n => console.log(n));
Enter fullscreen mode Exit fullscreen mode

You DO need to unsubscribe from:

  • interval() — ticks forever
  • fromEvent() — fires on every click/keypress forever
  • Custom Observables that never complete
  • BehaviorSubject / Subject streams 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 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 a Subscription object
  • 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 async pipe is the easiest way — it handles subscribe AND unsubscribe automatically
  • The takeUntil + Subject pattern 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)