DEV Community

Sarath
Sarath

Posted on

Fix Angular Subscription Memory Leaks with One Simple Decorator

So if you've been doing Angular development for a while, you've definitely run into this:

export class MyComponent implements OnInit {
  ngOnInit() {
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });

    this.dataService.getData().subscribe(data => {
      this.data = data;
    });

    // ... 10 more subscriptions
  }
}
Enter fullscreen mode Exit fullscreen mode

Looks fine, right? Well, not really. You just created a memory leak.

When the component gets destroyed, those subscriptions are still running in the background, eating up memory and sometimes causing weird bugs. In a big app with lots of components, this becomes a real problem.

The Traditional Solutions

  1. Manual Unsubscription (The Tedious Way)
export class MyComponent implements OnInit, OnDestroy {
  private userSubscription: Subscription;
  private dataSubscription: Subscription;
  private settingsSubscription: Subscription;
  // ... 10 more subscription properties

  ngOnInit() {
    this.userSubscription = this.userService.getUsers().subscribe(/*...*/);
    this.dataSubscription = this.dataService.getData().subscribe(/*...*/);
    this.settingsSubscription = this.settings.watch().subscribe(/*...*/);
  }

  ngOnDestroy() {
    this.userSubscription?.unsubscribe();
    this.dataSubscription?.unsubscribe();
    this.settingsSubscription?.unsubscribe();
    // ... 10 more unsubscribe calls
  }
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • Repetitive boilerplate The problems here are obvious:
  • Way too much boilerplate code
  • Really easy to forget unsubscribing from one or two
  • Tons of extra takeUntil Pattern (Better, But Still Boilerplate)
export class MyComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.userService.getUsers()
      .pipe(takeUntil(this.destroy$))
      .subscribe(/*...*/);

    this.dataService.getData()
      .pipe(takeUntil(this.destroy$))
      .subscribe(/*...*/);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

This is better, but you still have issues:

  • You need to remember to add takeUntil to every single subscription
  • Still writing the same ngOnDestroy code everywhere
  • Easy to forget the pattern on new components

3. Async Pipe (The Angular Way)

// Component
users$ = this.userService.getUsers();

// Template
<div *ngFor="let user of users$ | async">
  {{ user.name }}
</div>
Enter fullscreen mode Exit fullscreen mode

This works great for simple cases, but:

  • Can't use it for side effects
  • Complex logic gets really messy in templates
  • Not practical for a lot of real-world scenarios

My Solution: The AutoUnsubscribe Decorator

I got tired of writing the same cleanup code over and over, so I built this decorator:

@AutoUnsubscribe
@Component({
  selector: 'app-my-component',
  template: '...'
})
export class MyComponent implements OnInit {
  userSubscription: Subscription;
  dataSubscription: Subscription;
  settingsSubscription: Subscription;

  ngOnInit() {
    this.userSubscription = this.userService.getUsers().subscribe(/*...*/);
    this.dataSubscription = this.dataService.getData().subscribe(/*...*/);
    this.settingsSubscription = this.settings.watch().subscribe(/*...*/);
  }


}
Enter fullscreen mode Exit fullscreen mode

That's it. All subscriptions automatically cleaned up when the component is destroyed.

---literally it. All subscriptions get cleaned up automatically when the component is destroyed.

Implementation

import { Subscription } from 'rxjs';
/**
 * Decorator that automatically unsubscribes from all Subscription properties
 * when the component is destroyed.
 * 
 * @example
 * @AutoUnsubscribe
 * @Component({...})
 * export class MyComponent {
 *   dataSubscription: Subscription;
 * }
 */
export function AutoUnsubscribe(constructor: Function) {
  const original = constructor.prototype.ngOnDestroy;

  constructor.prototype.ngOnDestroy = function () {
    // Iterate through component properties
    for (const prop in this) {
      // Only check own properties (not inherited)
      if (!this.hasOwnProperty(prop)) {
        continue;
      }

      const property = this[prop];

      // Check if property is a Subscription instance
      if (property && property instanceof Subscription) {
        try {
          property.unsubscribe();
        } catch (err) {
          console.error(`Error unsubscribing from ${prop}:`, err);
        }
      }
    }

    // Call original ngOnDestroy if it exists
    if (original && typeof original === 'function') {
      original.apply(this);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

How It Works

Identifies Subscriptions** using instanceof check

  1. Automatically unsubscribes from each one
  2. The decorator hooks into the component's ngOnDestroy lifecycle
  3. It loops through all the component's properties
  4. Uses instanceof to identify Subscription objects
  5. Automatically calls unsubscribe on each one
  6. Still calls your original ngOnDestroy if you have one

The cool part is it's type-safe - it only unsubscribes actual Subscription objects, not just anything with an unsubscribe method.

before: ~15 lines of boilerplate

// After: One decorator

2. Impossible to Forget

No more "Oops, I forgot to unsubscribe from that one!"

3. Works with Custom ngOnDestroy

@AutoUnsubscribe
export class MyComponent implements OnDestroy {
  subscription: Subscription;

  ngOnDestroy() {
    // Your custom cleanup code
    console.log('Component destroyed');
    // Subscriptions still auto-unsubscribed!
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Zero Dependencies

Pure TypeScript. No external libraries needed.

Trade-offs

Just pure TypeScript. No libraries to install.

Trade-offs

Honestly, nothing is perfect. Here's what I've found:

Pros:

  • Really clean code
  • Follows DRY principle
  • Pretty hard to mess up
  • Type-safe

Cons:

  • The behavior isn't super explicit (some people don't like "magic")
  • Can be trickier to debug if something goes wrong
  • Your team needs to know how it works

My subscriptions in one component | @AutoUnsubscribe |

| Existing codebase cleanup | @AutoUnsubscribe |
| Team prefers explicit code | takeUntil pattern |


Real-World Impact

In our production Angular application:

Real-World Results

I've been using this for some time. Here's what happened

Pro Tips

Before: 2MB bundle size, occasional memory leaks showing up

  • After: 800KB bundle (went through and cleaned up unused imports while fixing subscriptions)
  • Time saved: About 10 minutes per component when refactoring
  • Memory leak bugs: Zero in the last 6 months

2. Combine Approaches

@AutoUnsubscribe
export class MyComponent {
  // Auto-unsubscribed
  dataSubscription: Subscription;

  // Template-based (no variable needed)
  users$ = this.userService.getUsers();
}
Enter fullscreen mode Exit fullscreen mode

3. Add to Your Utils Or Create a new library for angular and add it there

What do you use for handling subscriptions? I'd be curious to hear if there are better approaches out there.


Top comments (0)