ngOnInit & ngOnDestroy
ngOnInit and ngOnDestroy are the two lifecycle hooks you reach for most often. ngOnInit runs once after Angular has set up the component and bound its inputs, making it the correct place for initialization work that depends on those inputs. ngOnDestroy runs just before Angular removes the component, giving you a single, predictable spot to release resources. Getting these two right is the difference between a clean component and one that leaks subscriptions, timers, and event listeners.
When ngOnInit runs
The constructor is not the right place to read @Input() values—Angular has not assigned them yet when the constructor executes. ngOnInit is called once, after the first ngOnChanges and after all data-bound inputs have their initial values. That makes it the canonical hook for fetching data, deriving local state from inputs, and wiring up subscriptions.
To use it, implement the OnInit interface and define the method. The interface is optional at runtime but recommended—it lets TypeScript catch typos like ngOninit.
import { Component, Input, OnInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface Profile {
id: number;
name: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `<h2>{{ profile?.name ?? 'Loading…' }}</h2>`,
})
export class UserProfileComponent implements OnInit {
@Input({ required: true }) userId!: number;
private http = inject(HttpClient);
profile?: Profile;
ngOnInit(): void {
// userId is guaranteed to be bound here
this.http
.get<Profile>(`/api/users/${this.userId}`)
.subscribe((p) => (this.profile = p));
}
}
Tip: Keep constructors limited to dependency injection and field initialization. Do real work—HTTP calls, reading inputs, starting timers—in
ngOnInitso it happens after inputs are available and stays easy to test.
When ngOnDestroy runs
ngOnDestroy is called immediately before Angular destroys the component or directive, whether because it was removed from a @for/@if block, the user navigated away, or its host was destroyed. Anything you started in ngOnInit that outlives a single change-detection cycle should be torn down here: RxJS subscriptions, setInterval/setTimeout handles, manual DOM event listeners, and WebSocket connections.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-clock',
standalone: true,
template: `<p>Elapsed: {{ seconds }}s</p>`,
})
export class ClockComponent implements OnInit, OnDestroy {
seconds = 0;
private sub?: Subscription;
ngOnInit(): void {
this.sub = interval(1000).subscribe(() => this.seconds++);
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}
Forgetting the unsubscribe() keeps the interval running and the component instance referenced, so memory grows every time the component is created and removed.
Managing subscriptions cleanly
Tracking a Subscription field works, but modern Angular offers better options that reduce boilerplate and remove the chance of forgetting teardown.
| Approach | How it cleans up | Best for |
|---|---|---|
Subscription field + ngOnDestroy | Manual unsubscribe() | Legacy code, fine-grained control |
takeUntilDestroyed() | Auto-completes on destroy | Streams you keep subscribing to manually |
async pipe | Pipe unsubscribes on destroy | Values rendered directly in the template |
toSignal() | Cleans up with the injection context | Bridging RxJS into signal-based views |
The takeUntilDestroyed() operator (from @angular/core/rxjs-interop) hooks into the component’s destroy lifecycle automatically. Called in an injection context such as a field initializer or constructor, it needs no DestroyRef argument.
import { Component } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-ticker',
standalone: true,
template: `<p>Tick {{ count }}</p>`,
})
export class TickerComponent {
count = 0;
constructor() {
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe(() => this.count++);
}
}
For cleanup that is not tied to an observable—say, a manual event listener—inject DestroyRef and register a callback. This avoids implementing OnDestroy entirely.
import { Component, DestroyRef, inject } from '@angular/core';
@Component({
selector: 'app-resize-watcher',
standalone: true,
template: `<p>Width: {{ width }}px</p>`,
})
export class ResizeWatcherComponent {
width = window.innerWidth;
private destroyRef = inject(DestroyRef);
constructor() {
const onResize = () => (this.width = window.innerWidth);
window.addEventListener('resize', onResize);
this.destroyRef.onDestroy(() => window.removeEventListener('resize', onResize));
}
}
Verifying the order of execution
A quick logging example makes the sequence concrete. Toggling the component in and out of an @if block shows init running before the view appears and destroy running on removal.
Output:
UserProfileComponent: ngOnInit
UserProfileComponent: ngOnDestroy
Warning:
ngOnDestroydoes not fire if the entire browser tab is closed or refreshed—it only runs during Angular’s own teardown. For “before unload” cleanup (e.g. flushing analytics), use thewindowbeforeunload/pagehideevents, notngOnDestroy.
Best Practices
- Move input-dependent setup out of the constructor and into
ngOnInit, where bound inputs are guaranteed to be available. - Implement the
OnInitandOnDestroyinterfaces so TypeScript verifies the hook names. - Prefer
takeUntilDestroyed(), theasyncpipe, ortoSignal()over manualSubscriptionbookkeeping to avoid leaks. - Use
inject(DestroyRef).onDestroy()for non-observable cleanup like event listeners and timers instead of implementingOnDestroy. - Tear down everything you create: subscriptions, intervals, listeners, and sockets all belong in destroy logic.
- Do not rely on
ngOnDestroyfor tab-close cleanup—wirebeforeunloadfor that case.