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
- 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
takeUntilPattern (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
takeUntilto every single subscription - Still writing the same
ngOnDestroycode 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
- Automatically unsubscribes from each one
- The decorator hooks into the component's
ngOnDestroylifecycle - It loops through all the component's properties
- Uses
instanceofto identify Subscription objects - Automatically calls unsubscribe on each one
- Still calls your original
ngOnDestroyif 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.
Top comments (0)