Angular 15 ๐ ฐ๏ธ Lifecycle Hooks โ The Essentials
Every Angular component has a lifecycle โ a sequence of stages from creation to destruction. Angular calls lifecycle hooks at each stage: methods on the component class you can implement to run code at the right moment. Most components only need two of them โ ngOnInit for setup and ngOnDestroy for cleanup โ but the full set gives you control over every phase. This chapter covers the essential hooks: constructor, ngOnInit, ngOnDestroy, and how they fit together.
Key point: The constructor runs before Angular sets inputs โ use it only for dependency injection. ngOnInit runs once after the first input change โ use it for setup: fetching data, initializing state, starting subscriptions. ngOnDestroy runs just before the component is destroyed โ use it to clean up: unsubscribe, clear timers, release resources. Those three cover 95% of real code.
The lifecycle at a glance
A component moves through several stages, and Angular calls hooks at each.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Component created โ
โ โ โ
โ โผ โ
โ constructor โ
โ โ โ
โ โผ โ
โ ngOnChanges (first) โ
โ โ โ
โ โผ โ
โ ngOnInit โ
โ โ โ
โ โผ โ
โ (component runs) โ
โ โ โ
โ โผ โ
โ ngOnDestroy โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The essential hooks:
| Hook | When | Use for |
|---|---|---|
constructor | On instantiation | Dependency injection |
ngOnInit | Once after first ngOnChanges | Setup, data loading, subscriptions |
ngOnDestroy | Just before destruction | Cleanup โ unsubscribe, clear timers |
The full list (covered in the next chapter):
| Hook | When |
|---|---|
ngOnChanges | Input changes |
ngOnInit | After first input change |
ngDoCheck | Every CD cycle |
ngAfterContentInit | After content projection initializes |
ngAfterContentChecked | After every content check |
ngAfterViewInit | After view initializes |
ngAfterViewChecked | After every view check |
ngOnDestroy | Before destruction |
For most components, ngOnInit and ngOnDestroy are the only two you’ll implement.
Why hooks exist: Angular needs to tell your component when things happen โ when inputs are ready, when the view exists, when the component is about to go away. Without hooks, you’d have no place to run setup or cleanup. Hooks are the contract between your class and the framework’s lifecycle.
The constructor โ DI only
The constructor runs when Angular creates the component. It’s the first thing to run โ before inputs are set, before the template exists.
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user',
standalone: true,
template: `<p>{{ user()?.name }}</p>`
})
export class UserComponent {
private userService = inject(UserService);
user = signal<User | null>(null);
}
Or with constructor injection:
export class UserComponent {
constructor(private userService: UserService) {}
}
What the constructor can do:
- Inject dependencies (via
inject()or constructor params) - Assign readonly fields
- Nothing else meaningful
What the constructor can’t do:
- Read
@Inputvalues โ they’re not set yet - Access the template or view โ they don’t exist yet
- Safely trigger anything that depends on inputs
// โ Wrong
export class UserComponent {
@Input() userId!: number;
constructor() {
// userId is undefined here
this.loadUser(this.userId);
}
}
// โ
Right
export class UserComponent implements OnInit {
@Input() userId!: number;
ngOnInit(): void {
// userId is set here
this.loadUser(this.userId);
}
}
Reading inputs in the constructor: With signal inputs, the input signal exists in the constructor, but its bound value isn’t available until after change detection. Reading it in the constructor gives the default or throws for required. Use effect or ngOnInit for anything that depends on the actual value.
Async work in the constructor: Don’t. The constructor should be synchronous and fast. Load data in ngOnInit or an effect.
Why keep the constructor simple: The constructor runs during Angular’s change detection โ before the component is fully ready. If it does async work or accesses inputs, it either fails or introduces subtle bugs. Keeping it to DI only makes lifecycle behavior predictable.
ngOnInit โ setup
ngOnInit runs once, after Angular has set the inputs and before the first ngOnChanges cycle completes. It’s the right place for setup.
import { Component, OnInit, Input, inject } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `<p>{{ user?.name }}</p>`
})
export class UserCardComponent implements OnInit {
@Input() userId!: number;
user: User | null = null;
private userService = inject(UserService);
ngOnInit(): void {
this.loadUser();
}
private loadUser(): void {
this.userService.get(this.userId).subscribe(u => this.user = u);
}
}
What ngOnInit is for:
- Reading inputs โ they’re set
- Fetching data from services
- Setting up initial state
- Subscribing to observables
- Starting timers
- Anything that needs to run once, at the beginning
What ngOnInit is not for:
- Reacting to changes to inputs (that’s
ngOnChangesoreffect) - DOM access (that’s
ngAfterViewInit) - Cleanup (that’s
ngOnDestroy)
When it runs:
- Once per component instance
- After the first
ngOnChanges - Before the view is rendered
Signal inputs in ngOnInit:
userId = input.required<number>();
ngOnInit(): void {
const id = this.userId(); // โ
value available
this.load(id);
}
Signal inputs are readable in ngOnInit. The value is set.
Alternatives to ngOnInit:
For signal-based code, an effect in the constructor can replace much of what ngOnInit does:
userId = input.required<number>();
constructor() {
effect(() => {
this.load(this.userId());
});
}
The effect runs whenever userId changes โ including the initial value. It replaces both ngOnInit (first load) and ngOnChanges (subsequent loads).
When to use ngOnInit vs effect:
| Situation | Use |
|---|---|
| One-time setup | ngOnInit |
| Reactive to input changes | effect |
| Signal-based component | effect |
| Decorator-based component | ngOnInit |
Why
ngOnInitis the standard hook: It’s the first moment the component is fully initialized โ inputs set, dependencies injected. Everything you need is available. Before it, inputs aren’t ready; after it, it never runs again. That once-only, fully-ready moment is exactly what setup requires.
ngOnDestroy โ cleanup
ngOnDestroy runs once, just before Angular destroys the component. It’s where you release resources.
import { Component, OnDestroy, inject } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>{{ seconds }}</p>`
})
export class TimerComponent implements OnDestroy {
seconds = 0;
private sub = interval(1000).subscribe(n => this.seconds = n);
ngOnDestroy(): void {
this.sub.unsubscribe();
}
}
What ngOnDestroy is for:
- Unsubscribing from observables
- Clearing timers and intervals
- Canceling HTTP requests
- Detaching event listeners
- Cleaning up third-party libraries
What happens without cleanup:
- Memory leaks โ subscriptions keep running after the component is gone
- Stale callbacks โ the subscription tries to update a destroyed component
- Leaked intervals โ timers keep ticking
- Retained references โ large objects held longer than needed
Example of a leak:
export class LeakyComponent implements OnInit {
ngOnInit(): void {
interval(1000).subscribe(n => console.log(n)); // โ never unsubscribed
}
}
Every time the component is created, a new subscription starts. It never stops. Over time, the app has hundreds of running intervals, each holding a reference to a destroyed component.
Proper cleanup:
export class CleanComponent implements OnInit, OnDestroy {
private sub?: Subscription;
ngOnInit(): void {
this.sub = interval(1000).subscribe(n => console.log(n));
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}
Multiple subscriptions โ Subscription composition:
private subs = new Subscription();
ngOnInit(): void {
this.subs.add(this.a$.subscribe(...));
this.subs.add(this.b$.subscribe(...));
this.subs.add(this.c$.subscribe(...));
}
ngOnDestroy(): void {
this.subs.unsubscribe(); // unsubscribes all
}
takeUntilDestroyed โ the modern pattern:
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class ModernComponent {
constructor() {
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe(n => console.log(n));
}
}
No ngOnDestroy needed โ takeUntilDestroyed completes the subscription when the component is destroyed. It uses the component’s injector’s DestroyRef.
Async pipe โ no cleanup needed:
@Component({
template: `{{ user$ | async }}`
})
export class AsyncComponent {
user$ = this.userService.getCurrent();
}
The async pipe subscribes and unsubscribes automatically. No ngOnDestroy required.
Why cleanup is critical: Angular destroys components when they leave the view โ navigating away, closing a modal, changing a route. Any subscription or timer still running after that keeps holding memory and, worse, keeps trying to update a component that no longer exists. Cleanup is the contract that says “I’m done, release everything.”
When hooks run โ a concrete trace
Let’s trace a component through its lifecycle.
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
@Component({
selector: 'app-demo',
standalone: true,
template: `<p>{{ name }}</p>`
})
export class DemoComponent implements OnInit, OnDestroy {
@Input() name = '';
constructor() {
console.log('1. constructor');
}
ngOnInit(): void {
console.log('2. ngOnInit โ name is', this.name);
}
ngOnDestroy(): void {
console.log('3. ngOnDestroy');
}
}
Parent:
<app-demo *ngIf="show" name="Alice"></app-demo>
<button (click)="show = !show">Toggle</button>
When show becomes true:
1. constructor
2. ngOnInit โ name is Alice
When show becomes false:
3. ngOnDestroy
When show becomes true again:
1. constructor โ new instance
2. ngOnInit
Each time the component is toggled, a new instance is created. ngOnInit and ngOnDestroy run on that instance.
Key insight: ngOnInit and ngOnDestroy are per-instance. A component that appears and disappears repeatedly creates a new instance each time โ and each one runs the full lifecycle.
What’s cleaned up when the component is destroyed:
- The component instance
- Its template and view
- Any subscriptions not unsubscribed in
ngOnDestroy - Any timers not cleared
Why per-instance lifecycle matters: A component isn’t reused. When it leaves the DOM, it’s destroyed. A new one is created when it comes back. This makes lifecycle predictable โ each instance runs
ngOnInitonce andngOnDestroyonce โ but it also means cleanup must happen in every instance. Forget it once, and every visit leaks a subscription.
Hooks and change detection
Hooks run during change detection. Knowing the order helps reason about behavior.
Order for a component:
constructorโ DI availablengOnChangesโ if inputs changedngOnInitโ once, after firstngOnChanges- Template rendered
ngAfterViewInitโ after view is rendered- Subsequent CD cycles โ
ngDoCheck,ngAfterContentChecked,ngAfterViewChecked ngOnDestroyโ on destruction
For the essential hooks:
| Hook | Runs |
|---|---|
constructor | Before inputs |
ngOnInit | After first inputs, before view |
ngOnDestroy | On destruction |
Under OnPush change detection: ngOnInit still runs once. ngOnChanges fires when an input reference changes. Any signal read in the template triggers re-render.
Under Default change detection: Every CD cycle checks the component. ngOnInit still runs only once โ CD doesn’t re-run it.
Async work in ngOnInit: If you subscribe to an observable and update state, Angular’s CD picks up the change and re-renders. You don’t need to trigger CD manually โ unless you use runOutsideAngular.
Why understanding order matters: If you try to read an input in the constructor, it fails. If you try to access a
@ViewChildinngOnInit, it’s undefined โ the view doesn’t exist yet. Knowing what’s available when saves debugging time. The rule: inputs inngOnInit, view inngAfterViewInit, and cleanup inngOnDestroy.
Signal-based cleanup with takeUntilDestroyed
Angular 16+ provides takeUntilDestroyed, an RxJS operator that completes a subscription when the component is destroyed.
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-ticker',
standalone: true,
template: `<p>{{ n }}</p>`
})
export class TickerComponent {
n = 0;
constructor() {
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe(v => this.n = v);
}
}
No ngOnDestroy needed. The subscription completes automatically when the component is destroyed.
How it works: The operator uses the current injection context to find the component’s DestroyRef. When DestroyRef fires onDestroy, the subscription completes.
When it must be called in the constructor: Without arguments, takeUntilDestroyed must be called in an injection context โ the constructor is the standard place. To use it elsewhere, pass the DestroyRef explicitly:
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
interval(1000)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(...);
}
Advantages over ngOnDestroy:
- No cleanup method needed
- Composes with other operators
- Impossible to forget (the operator is right there in the pipe)
- Works with any observable source
When to still use ngOnDestroy:
- Cleaning up non-RxJS resources (event listeners, timers, third-party libs)
- Calling a service method that requires explicit teardown
- Coordinating complex cleanup logic
Why
takeUntilDestroyedmatters: Forgetful cleanup is the number-one source of memory leaks in Angular.takeUntilDestroyedmakes cleanup the default โ you write it once in the pipe, and Angular handles the rest. For signal-based and RxJS-heavy code, it’s the standard pattern.
A full example
A component that loads user data on init and cleans up on destroy.
import {
Component, Input, OnInit, OnDestroy, inject, signal
} from '@angular/core';
import { Subscription, switchMap } from 'rxjs';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-detail',
standalone: true,
template: `
@if (loading()) {
<p>Loading...</p>
} @else if (user()) {
<h2>{{ user()!.name }}</h2>
<p>{{ user()!.email }}</p>
} @else {
<p>User not found.</p>
}
`
})
export class UserDetailComponent implements OnInit, OnDestroy {
@Input({ required: true }) userId!: number;
user = signal<User | null>(null);
loading = signal(false);
private userService = inject(UserService);
private sub?: Subscription;
ngOnInit(): void {
this.loading.set(true);
this.sub = this.userService.get(this.userId).subscribe({
next: (u) => {
this.user.set(u);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
}
});
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}
What happens:
- Parent renders
<app-user-detail [userId]="42"> - Angular creates the component, injects
UserService ngOnInitruns โ inputs are set,loadstarts- Response arrives โ signals update, view re-renders
- User navigates away โ Angular destroys the component
ngOnDestroyruns โ subscription is unsubscribed
What would break without cleanup: If ngOnDestroy didn’t unsubscribe, the HTTP request would keep the component reference alive. If the user navigated away before the response arrived, the subscription would try to update a destroyed component.
Modern equivalent with takeUntilDestroyed:
export class UserDetailComponent {
userId = input.required<number>();
user = signal<User | null>(null);
private userService = inject(UserService);
constructor() {
effect(() => {
const id = this.userId();
this.userService.get(id)
.pipe(takeUntilDestroyed())
.subscribe(u => this.user.set(u));
});
}
}
No ngOnInit, no ngOnDestroy โ the effect reacts to input changes, and takeUntilDestroyed handles cleanup.
Why this shape: It’s the essential lifecycle in one component โ setup in
ngOnInit, cleanup inngOnDestroy. The modern version replaces both with a single effect andtakeUntilDestroyed. Both patterns are valid; the second is where Angular is heading.
Complete Example Session
# ============================================
# PART 1: GENERATE A COMPONENT
# ============================================
ng generate component lifecycle-demo
# [ CREATE src/app/lifecycle-demo/lifecycle-demo.component.ts ]
# ============================================
# PART 2: TRACE THE LIFECYCLE
# ============================================
cat > src/app/lifecycle-demo/lifecycle-demo.component.ts << 'EOF'
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<p>{{ message }}</p>`
})
export class LifecycleDemoComponent implements OnInit, OnDestroy {
@Input() name = '';
message = '';
constructor() {
console.log('1. constructor');
}
ngOnInit(): void {
console.log('2. ngOnInit โ name =', this.name);
this.message = `Hello, ${this.name}!`;
}
ngOnDestroy(): void {
console.log('3. ngOnDestroy');
}
}
EOF
# ============================================
# PART 3: TOGGLE IT FROM A PARENT
# ============================================
cat > src/app/demo/demo.component.ts << 'EOF'
import { Component } from '@angular/core';
import { LifecycleDemoComponent } from '../lifecycle-demo/lifecycle-demo.component';
@Component({
selector: 'app-demo',
standalone: true,
imports: [LifecycleDemoComponent],
template: `
<button (click)="show = !show">Toggle</button>
@if (show) {
<app-lifecycle-demo name="Alice"></app-lifecycle-demo>
}
`
})
export class DemoComponent {
show = true;
}
EOF
# ============================================
# PART 4: CLEANUP WITH SUBSCRIPTION
# ============================================
cat > src/app/timer/timer.component.ts << 'EOF'
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>{{ seconds }}s</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = 0;
private sub?: Subscription;
ngOnInit(): void {
this.sub = interval(1000).subscribe(n => this.seconds = n);
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}
EOF
# ============================================
# PART 5: MODERN CLEANUP
# ============================================
cat > src/app/modern-timer/modern-timer.component.ts << 'EOF'
import { Component } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-modern-timer',
standalone: true,
template: `<p>{{ seconds }}s</p>`
})
export class ModernTimerComponent {
seconds = 0;
constructor() {
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe(n => this.seconds = n);
}
}
EOF
# ============================================
# PART 6: SERVE AND OBSERVE
# ============================================
ng serve
# [ Local: http://localhost:4200/ ]
# Console output when toggling:
# [ 1. constructor ]
# [ 2. ngOnInit โ name = Alice ]
# [ 3. ngOnDestroy ]
# [ 1. constructor ]
# [ 2. ngOnInit โ name = Alice ]
# ...
Every pattern is exercised โ constructor for DI, ngOnInit for setup, ngOnDestroy for cleanup, and takeUntilDestroyed for the modern approach.
Why this exercise: Watching the console output as you toggle the component makes the lifecycle concrete. You see
ngOnInitandngOnDestroyfire on each instance. It’s the fastest way to internalize the pattern.
Quick Reference
Essential Hooks
| Hook | Runs | Use for |
|---|---|---|
constructor | On creation | DI only |
ngOnInit | Once after inputs | Setup |
ngOnDestroy | Before destruction | Cleanup |
Constructor Rules
| Allowed | Not allowed |
|---|---|
| Dependency injection | Reading @Input values |
| Field initialization | Accessing the view |
| Signal declarations | Async work |
ngOnInit
| Property | Value |
|---|---|
| Runs | Once per instance |
| After | First ngOnChanges |
| Before | View render |
| Inputs available | โ |
| View available | โ |
ngOnDestroy
| Property | Value |
|---|---|
| Runs | Once per instance |
| Before | Destruction |
| Use for | Cleanup |
What to Clean Up
| Resource | Cleanup |
|---|---|
| Subscription | .unsubscribe() |
setInterval | clearInterval() |
setTimeout | clearTimeout() |
| Event listener | removeEventListener() |
| WebSocket | .close() |
| Third-party lib | Its teardown API |
Interfaces
| Hook | Interface |
|---|---|
ngOnInit | OnInit |
ngOnDestroy | OnDestroy |
ngOnChanges | OnChanges |
ngAfterViewInit | AfterViewInit |
Lifecycle Order (Essentials)
| Order | Hook |
|---|---|
| 1 | constructor |
| 2 | ngOnChanges (if inputs) |
| 3 | ngOnInit |
| 4 | View rendered |
| 5 | ngOnDestroy |
Constructor vs ngOnInit
| Aspect | Constructor | ngOnInit |
|---|---|---|
| When | On creation | After inputs |
| Inputs | โ undefined | โ set |
| DI | โ | โ |
| Template | โ | โ (view not ready) |
| Async | โ avoid | โ |
| Runs | Once | Once |
Cleanup Approaches
| Approach | When |
|---|---|
ngOnDestroy + unsubscribe | Classic |
takeUntilDestroyed | RxJS modern |
async pipe | Template |
Subs container | Multiple subs |
DestroyRef.onDestroy | Custom |
takeUntilDestroyed
| Use | Notes |
|---|---|
| In constructor | No argument |
| Outside constructor | Pass DestroyRef |
| On any observable | Completes on destroy |
Replaces ngOnDestroy | For RxJS cleanup |
ngOnInit vs effect
| Need | Hook |
|---|---|
| One-time setup | ngOnInit |
| React to input changes | effect |
| Signal-based | effect |
| Decorator-based | ngOnInit |
Common Mistakes
| Mistake | Fix |
|---|---|
| Reading inputs in constructor | Use ngOnInit |
| Async work in constructor | Move to ngOnInit |
| Forgetting to unsubscribe | Use ngOnDestroy or takeUntilDestroyed |
Accessing view in ngOnInit | Use ngAfterViewInit |
Heavy work in ngOnInit | Move to a service or lazy-load |
| Not implementing the interface | Add implements OnInit |
Best Practices
โ Do This:
// Use the constructor for DI only
constructor(private userService: UserService) {} // โ
// Or:
private userService = inject(UserService); // โ
// Use ngOnInit for setup
ngOnInit(): void {
this.load();
} // โ
// Read inputs in ngOnInit
ngOnInit(): void {
const id = this.userId; // โ
} // โ
// Implement the interface
export class MyComponent implements OnInit, OnDestroy { } // โ
// Unsubscribe in ngOnDestroy
ngOnDestroy(): void {
this.sub?.unsubscribe();
} // โ
// Prefer takeUntilDestroyed for RxJS
interval(1000).pipe(takeUntilDestroyed()).subscribe(...); // โ
// Use async pipe when possible
user$ = this.service.getUser(); // โ
// Use a Subscription container for many subs
private subs = new Subscription(); // โ
โ Don’t Do This:
// Don't read inputs in the constructor
constructor() {
console.log(this.userId); // โ undefined // โ
}
// Don't do async work in the constructor
constructor() {
this.service.get().subscribe(...); // โ ๏ธ // โ ๏ธ
}
// Don't forget to unsubscribe
ngOnInit() {
interval(1000).subscribe(...); // โ never cleaned // โ
}
// Don't access @ViewChild in ngOnInit
ngOnInit() {
this.child.doSomething(); // โ view not ready // โ
}
// Don't put setup in the constructor
constructor() {
this.loadData(); // โ move to ngOnInit // โ
}
// Don't do heavy sync work in ngOnInit
ngOnInit() {
for (let i = 0; i < 1e8; i++) {} // โ ๏ธ blocks first render // โ ๏ธ
}
// Don't forget the interface
export class MyComponent {
ngOnInit() {} // โ ๏ธ works but no type check // โ ๏ธ
}
// Don't leak memory with nested subscriptions
ngOnInit() {
this.a$.subscribe(() => {
this.b$.subscribe(() => {}); // โ b never unsubscribed // โ
});
}
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Reading inputs in constructor | Undefined | Use ngOnInit |
| Async work in constructor | Runs too early | Move to ngOnInit |
Forgetting unsubscribe | Memory leak | ngOnDestroy or takeUntilDestroyed |
Accessing view in ngOnInit | Undefined | Use ngAfterViewInit |
Not implementing OnDestroy | Cleanup skipped silently | Always implement |
| Missing interface | No type check | Add implements OnInit |
Heavy sync work in ngOnInit | Slow first render | Move to a service or async |
| Nested subscriptions | Inner never cleaned up | Flatten with switchMap |
Subscribing to async pipe observable | Double subscription | Use async pipe only |
Real-World Examples
1. Basic ngOnInit
ngOnInit(): void {
console.log('Component initialized');
}
2. Fetch data on init
ngOnInit(): void {
this.userService.get(this.userId).subscribe(u => this.user = u);
}
3. Read inputs in ngOnInit
@Input() userId!: number;
ngOnInit(): void {
console.log(this.userId); // โ
set
}
4. Basic ngOnDestroy
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
5. Timer cleanup
private id?: number;
ngOnInit(): void {
this.id = window.setInterval(...);
}
ngOnDestroy(): void {
if (this.id) clearInterval(this.id);
}
6. Subscription container
private subs = new Subscription();
ngOnInit(): void {
this.subs.add(this.a$.subscribe());
this.subs.add(this.b$.subscribe());
}
ngOnDestroy(): void {
this.subs.unsubscribe();
}
7. takeUntilDestroyed
constructor() {
interval(1000).pipe(takeUntilDestroyed()).subscribe(...);
}
8. With DestroyRef outside constructor
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.obs.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(...);
}
9. Async pipe โ no cleanup
user$ = this.service.getUser();
10. Implement multiple interfaces
export class X implements OnInit, OnDestroy { }
11. Signal-based effect instead of ngOnInit
constructor() {
effect(() => this.load(this.userId()));
}
12. Clear timeout
ngOnDestroy(): void {
if (this.timeoutId) clearTimeout(this.timeoutId);
}
13. Remove event listener
ngOnInit(): void {
window.addEventListener('resize', this.onResize);
}
ngOnDestroy(): void {
window.removeEventListener('resize', this.onResize);
}
14. Close WebSocket
ngOnDestroy(): void {
this.socket?.close();
}
15. Multiple hooks in one class
export class X implements OnInit, OnDestroy {
ngOnInit(): void { }
ngOnDestroy(): void { }
}
16. Guard against destroyed component
private destroyed = false;
ngOnDestroy(): void {
this.destroyed = true;
}
subscribe() {
this.obs.subscribe(v => {
if (!this.destroyed) this.value = v;
});
}
17. ngOnInit with signals
userId = input.required<number>();
ngOnInit(): void {
this.load(this.userId()); // โ
}
18. ngOnDestroy for third-party
ngOnDestroy(): void {
this.chart?.destroy();
}
19. Combine setup and cleanup
ngOnInit(): void {
this.sub = this.data$.subscribe();
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
20. Lifecycle with OnPush
@Component({ changeDetection: ChangeDetectionStrategy.OnPush })
export class X implements OnInit, OnDestroy { }
Visual: Lifecycle Order
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. constructor โ
โ โข DI available โ
โ โข inputs NOT set โ
โ โข view NOT ready โ
โ โ
โ 2. ngOnChanges (first) โ
โ โข inputs set โ
โ โ
โ 3. ngOnInit โ
โ โข inputs available โ
โ โข view NOT ready โ
โ โข once per instance โ
โ โ
โ 4. View rendered โ
โ โ
โ 5. ngOnDestroy โ
โ โข just before removal โ
โ โข cleanup โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: What’s Available When
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ constructor โ
โ โโโ DI โ
โ
โ โโโ inputs โ โ
โ โโโ view โ โ
โ โ
โ ngOnInit โ
โ โโโ DI โ
โ
โ โโโ inputs โ
โ
โ โโโ view โ โ
โ โ
โ ngAfterViewInit โ
โ โโโ DI โ
โ
โ โโโ inputs โ
โ
โ โโโ view โ
โ
โ โ
โ ngOnDestroy โ
โ โโโ DI โ
(careful) โ
โ โโโ inputs โ
โ
โ โโโ view โ
(may be destroyed) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Cleanup Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Classic โ ngOnDestroy โ
โ โ
โ private sub?: Subscription; โ
โ โ
โ ngOnInit() { โ
โ this.sub = obs.subscribe(...); โ
โ } โ
โ โ
โ ngOnDestroy() { โ
โ this.sub?.unsubscribe(); โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Modern โ takeUntilDestroyed โ
โ โ
โ constructor() { โ
โ obs โ
โ .pipe(takeUntilDestroyed()) โ
โ .subscribe(...); โ
โ } โ
โ โ
โ No ngOnDestroy needed โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Template โ async pipe โ
โ โ
โ user$ = this.service.get(); โ
โ โ
โ {{ user$ | async }} โ
โ โ
โ Auto unsubscribe โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Memory Leak Example
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ No cleanup โ
โ โ
โ ngOnInit() { โ
โ interval(1000).subscribe(...) โ
โ } โโ โ
โ Component destroyed โ subscription runs on โ
โ Create again โ another subscription โ
โ Result: N subscriptions, N leaks โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
With cleanup โ
โ โ
โ ngOnInit() { โ
โ this.sub = interval(1000).subscribe(...) โ
โ } โ
โ โ
โ ngOnDestroy() { โ
โ this.sub.unsubscribe() โ
โ } โ
โ โ
โ Component destroyed โ subscription stops โ
โ Create again โ fresh subscription โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: ngOnInit vs effect
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ngOnInit โ
โ โ
โ โข Runs once โ
โ โข After first inputs โ
โ โข Doesn't re-run on input changes โ
โ โ
โ Use for: one-time setup โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ effect โ
โ โ
โ โข Runs after first inputs โ
โ โข Re-runs when tracked signals change โ
โ โข Automatic dependency tracking โ
โ โ
โ Use for: react to input changes โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Toggle Lifecycle
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ show = true โ
โ โ โ
โ โผ โ
โ constructor โโโบ ngOnInit โ
โ โ
โ show = false โ
โ โ โ
โ โผ โ
โ ngOnDestroy โ
โ โ
โ show = true again โ
โ โ โ
โ โผ โ
โ constructor โโโบ ngOnInit โ new instance โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Cleanup Checklist
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Before component is destroyed: โ
โ โ
โ [ ] Subscriptions unsubscribed โ
โ [ ] Intervals cleared โ
โ [ ] Timeouts cleared โ
โ [ ] Event listeners removed โ
โ [ ] WebSockets closed โ
โ [ ] Third-party libs destroyed โ
โ [ ] Timers stopped โ
โ [ ] Observables completed โ
โ โ
โ Or: use takeUntilDestroyed / async pipe โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Hook | When | Use for |
|---|---|---|
constructor | On creation | Dependency injection |
ngOnInit | Once after inputs | Setup, data loading, subscriptions |
ngOnDestroy | Before destruction | Cleanup |
Key takeaways:
- The constructor runs first โ use it only for DI; inputs aren’t set
ngOnInitruns once, after inputs are available โ the right place for setupngOnDestroyruns once, before destruction โ the right place for cleanup- Read inputs in
ngOnInit, never the constructor - Don’t do async work in the constructor โ move it to
ngOnInit - Unsubscribe from observables in
ngOnDestroyโ or usetakeUntilDestroyed - Use the
asyncpipe in templates to avoid manual unsubscribe - Implement the interface (
OnInit,OnDestroy) for type checking takeUntilDestroyedis the modern RxJS cleanup pattern- Signal-based components can replace
ngOnInitwitheffect - Cleanup also means timers, event listeners, WebSockets, and third-party libs
- Each toggle creates a new instance โ every instance runs the full lifecycle
Remember: Every component has a lifecycle. Three hooks cover most of what you’ll ever need: constructor for DI, ngOnInit for setup, ngOnDestroy for cleanup. Know when each runs and what’s available at that moment. Read inputs in ngOnInit, not the constructor. Clean up everything in ngOnDestroy โ subscriptions, timers, listeners, third-party resources. Or use takeUntilDestroyed and let Angular handle RxJS cleanup. Getting the lifecycle right is what keeps your app from leaking memory and from crashing when components disappear.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!