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