# Solving Angular's Subscription Memory Leak Problem with a Simple Decorator

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5175

    #1

    # Solving Angular's Subscription Memory Leak Problem with a 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
    }
    }







    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
    }
    }







    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();
    }
    }







    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>







    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(/*...*/);
    }


    }







    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);
    }
    };
    }







    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!
    }
    }







    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();
    }







    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.







    More...
Working...