DestroyRef & takeUntilDestroyed (v16)
📚 What is DestroyRef?
DestroyRef and takeUntilDestroyed simplify cleanup:
- DestroyRef: Injectable that lets you register cleanup callbacks
- takeUntilDestroyed(): RxJS operator that auto-unsubscribes on destroy
- No OnDestroy needed: Cleaner code without implementing interface
- Works in services: Not limited to components
- Injection context: Must be called in injection context (constructor/field)
🎯 Interview Questions
- Q1: What problem does takeUntilDestroyed solve?
- A: Automatically unsubscribes from observables when component is destroyed, preventing memory leaks without manual unsubscribe logic.
- Q2: When must takeUntilDestroyed be called?
- A: In injection context: constructor, field initializer, or inside runInInjectionContext().
- Q3: How is DestroyRef different from ngOnDestroy?
- A: DestroyRef is injectable and can be used in services/functions. onDestroy callbacks can be registered from anywhere with access to DestroyRef.
- Q4: What are toSignal and toObservable?
- A: toSignal converts Observable to Signal. toObservable converts Signal to Observable. Part of rxjs-interop.
🔧 Live Demo - takeUntilDestroyed
Auto-cleaned Timer (takeUntilDestroyed)
0 seconds
This timer uses takeUntilDestroyed() - no manual unsubscribe needed! Navigate away and it auto-cleans up.
DestroyRef Callbacks
Registered 0 cleanup callbacks
Check console when you navigate away - callbacks will fire!
🔄 Old vs New Cleanup Patterns
❌ Old Way (Manual)
// OLD: Manual subscription management
export class OldComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}✅ New Way (v16+)
// NEW: takeUntilDestroyed (v16+)
export class NewComponent {
constructor() {
interval(1000).pipe(
takeUntilDestroyed() // That's it!
).subscribe();
}
}🔄 RxJS Interop (Signal ↔ Observable)
toSignal(observable$)Convert Observable to Signal. Auto-subscribes and unsubscribes.
toObservable(signal)Convert Signal to Observable. Emits on signal changes.
💻 DestroyRef Code Examples
import { DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
// ===== takeUntilDestroyed() =====
@Component({...})
export class TimerComponent {
constructor() {
// Must be in injection context (constructor or field initializer)
interval(1000).pipe(
takeUntilDestroyed() // Auto-unsubscribes on destroy!
).subscribe(val => console.log(val));
}
}
// ===== DestroyRef for Custom Cleanup =====
@Component({...})
export class ResourceComponent {
private destroyRef = inject(DestroyRef);
connect() {
const socket = new WebSocket('ws://...');
// Register cleanup callback
this.destroyRef.onDestroy(() => {
socket.close();
console.log('Socket closed');
});
}
}
// ===== In Services =====
@Injectable({ providedIn: 'root' })
export class PollingService {
private destroyRef = inject(DestroyRef);
startPolling() {
interval(5000).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.fetchData());
}
}
// ===== toSignal - Observable to Signal =====
@Component({...})
export class DataComponent {
private http = inject(HttpClient);
// Convert Observable to Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] } // Required for sync access
);
// In template: {{ users() }}
}
// ===== toObservable - Signal to Observable =====
@Component({...})
export class SearchComponent {
searchTerm = signal('');
// Convert Signal to Observable
searchTerm$ = toObservable(this.searchTerm);
constructor() {
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.search(term)),
takeUntilDestroyed()
).subscribe();
}
}
// ===== Outside Injection Context =====
import { runInInjectionContext, Injector } from '@angular/core';
@Component({...})
export class LateInitComponent {
private injector = inject(Injector);
initLater() {
// Can't use takeUntilDestroyed() directly here
// Use runInInjectionContext instead:
runInInjectionContext(this.injector, () => {
interval(1000).pipe(
takeUntilDestroyed()
).subscribe();
});
}
}