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
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:
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:
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:
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
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:
Cons:
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
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...
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 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
- Automatically unsubscribes from each one
- The decorator hooks into the component's ngOnDestroy lifecycle
- It loops through all the component's properties
- Uses instanceof to identify Subscription objects
- Automatically calls unsubscribe on each one
- 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...