| |

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:

HookWhenUse for
constructorOn instantiationDependency injection
ngOnInitOnce after first ngOnChangesSetup, data loading, subscriptions
ngOnDestroyJust before destructionCleanup โ€” unsubscribe, clear timers

The full list (covered in the next chapter):

HookWhen
ngOnChangesInput changes
ngOnInitAfter first input change
ngDoCheckEvery CD cycle
ngAfterContentInitAfter content projection initializes
ngAfterContentCheckedAfter every content check
ngAfterViewInitAfter view initializes
ngAfterViewCheckedAfter every view check
ngOnDestroyBefore 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 @Input values โ€” 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 ngOnChanges or effect)
  • 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:

SituationUse
One-time setupngOnInit
Reactive to input changeseffect
Signal-based componenteffect
Decorator-based componentngOnInit

Why ngOnInit is 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 ngOnInit once and ngOnDestroy once โ€” 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:

  1. constructor โ€” DI available
  2. ngOnChanges โ€” if inputs changed
  3. ngOnInit โ€” once, after first ngOnChanges
  4. Template rendered
  5. ngAfterViewInit โ€” after view is rendered
  6. Subsequent CD cycles โ€” ngDoCheck, ngAfterContentChecked, ngAfterViewChecked
  7. ngOnDestroy โ€” on destruction

For the essential hooks:

HookRuns
constructorBefore inputs
ngOnInitAfter first inputs, before view
ngOnDestroyOn 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 @ViewChild in ngOnInit, it’s undefined โ€” the view doesn’t exist yet. Knowing what’s available when saves debugging time. The rule: inputs in ngOnInit, view in ngAfterViewInit, and cleanup in ngOnDestroy.


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 takeUntilDestroyed matters: Forgetful cleanup is the number-one source of memory leaks in Angular. takeUntilDestroyed makes 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:

  1. Parent renders <app-user-detail [userId]="42">
  2. Angular creates the component, injects UserService
  3. ngOnInit runs โ€” inputs are set, load starts
  4. Response arrives โ€” signals update, view re-renders
  5. User navigates away โ€” Angular destroys the component
  6. ngOnDestroy runs โ€” 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 in ngOnDestroy. The modern version replaces both with a single effect and takeUntilDestroyed. 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 ngOnInit and ngOnDestroy fire on each instance. It’s the fastest way to internalize the pattern.


Quick Reference

Essential Hooks

HookRunsUse for
constructorOn creationDI only
ngOnInitOnce after inputsSetup
ngOnDestroyBefore destructionCleanup

Constructor Rules

AllowedNot allowed
Dependency injectionReading @Input values
Field initializationAccessing the view
Signal declarationsAsync work

ngOnInit

PropertyValue
RunsOnce per instance
AfterFirst ngOnChanges
BeforeView render
Inputs availableโœ…
View availableโŒ

ngOnDestroy

PropertyValue
RunsOnce per instance
BeforeDestruction
Use forCleanup

What to Clean Up

ResourceCleanup
Subscription.unsubscribe()
setIntervalclearInterval()
setTimeoutclearTimeout()
Event listenerremoveEventListener()
WebSocket.close()
Third-party libIts teardown API

Interfaces

HookInterface
ngOnInitOnInit
ngOnDestroyOnDestroy
ngOnChangesOnChanges
ngAfterViewInitAfterViewInit

Lifecycle Order (Essentials)

OrderHook
1constructor
2ngOnChanges (if inputs)
3ngOnInit
4View rendered
5ngOnDestroy

Constructor vs ngOnInit

AspectConstructorngOnInit
WhenOn creationAfter inputs
InputsโŒ undefinedโœ… set
DIโœ…โœ…
TemplateโŒโŒ (view not ready)
AsyncโŒ avoidโœ…
RunsOnceOnce

Cleanup Approaches

ApproachWhen
ngOnDestroy + unsubscribeClassic
takeUntilDestroyedRxJS modern
async pipeTemplate
Subs containerMultiple subs
DestroyRef.onDestroyCustom

takeUntilDestroyed

UseNotes
In constructorNo argument
Outside constructorPass DestroyRef
On any observableCompletes on destroy
Replaces ngOnDestroyFor RxJS cleanup

ngOnInit vs effect

NeedHook
One-time setupngOnInit
React to input changeseffect
Signal-basedeffect
Decorator-basedngOnInit

Common Mistakes

MistakeFix
Reading inputs in constructorUse ngOnInit
Async work in constructorMove to ngOnInit
Forgetting to unsubscribeUse ngOnDestroy or takeUntilDestroyed
Accessing view in ngOnInitUse ngAfterViewInit
Heavy work in ngOnInitMove to a service or lazy-load
Not implementing the interfaceAdd 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

PitfallProblemSolution
Reading inputs in constructorUndefinedUse ngOnInit
Async work in constructorRuns too earlyMove to ngOnInit
Forgetting unsubscribeMemory leakngOnDestroy or takeUntilDestroyed
Accessing view in ngOnInitUndefinedUse ngAfterViewInit
Not implementing OnDestroyCleanup skipped silentlyAlways implement
Missing interfaceNo type checkAdd implements OnInit
Heavy sync work in ngOnInitSlow first renderMove to a service or async
Nested subscriptionsInner never cleaned upFlatten with switchMap
Subscribing to async pipe observableDouble subscriptionUse 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

HookWhenUse for
constructorOn creationDependency injection
ngOnInitOnce after inputsSetup, data loading, subscriptions
ngOnDestroyBefore destructionCleanup

Key takeaways:

  • The constructor runs first โ€” use it only for DI; inputs aren’t set
  • ngOnInit runs once, after inputs are available โ€” the right place for setup
  • ngOnDestroy runs 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 use takeUntilDestroyed
  • Use the async pipe in templates to avoid manual unsubscribe
  • Implement the interface (OnInit, OnDestroy) for type checking
  • takeUntilDestroyed is the modern RxJS cleanup pattern
  • Signal-based components can replace ngOnInit with effect
  • 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!