Lifecycle Hooks

Chapter 12
Lifecycle Hooks
Running code at specific moments in a component's life — Angular's answer to useEffect's timing

React's useEffect (Fundamentals Chapter 8) collapsed "run on mount," "run on update," and "clean up on unmount" into one hook differentiated by its dependency array. Angular splits these into separate, explicitly-named lifecycle hook methods — each a method with a fixed name that Angular calls at a specific moment. A component opts into one by implementing the matching interface and writing the method.

ngOnInit — Setup After Creation

import { Component, OnInit, inject } from '@angular/core'; export class UserListComponent implements OnInit { private userService = inject(UserService); users: User[] = []; ngOnInit() { this.userService.getUsers().subscribe((u) => (this.users = u)); } }

ngOnInit runs once, right after Angular has created the component and set its initial @Input values — the standard place for setup work like an initial data fetch (used already in Chapter 11). It's the direct equivalent of React's useEffect(() => {...}, []) with an empty dependency array. Implementing OnInit isn't strictly required for the method to run, but doing so lets TypeScript catch a misspelled ngOnInit — strongly recommended.

Why Not the Constructor?

A natural question: why not just do setup in the class constructor? The constructor runs when the object is first created, before Angular has finished wiring it up — specifically before @Input properties have their values. Putting data fetching or any logic depending on inputs in ngOnInit guarantees those inputs are ready. The convention: the constructor is for dependency injection only (or just use inject()); ngOnInit is for actual initialization logic.

ngOnChanges — Responding to Input Changes

import { OnChanges, SimpleChanges, Input } from '@angular/core'; export class ChartComponent implements OnChanges { @Input() data: number[] = []; ngOnChanges(changes: SimpleChanges) { if (changes['data']) { console.log('data changed from', changes['data'].previousValue, 'to', changes['data'].currentValue); } } }

ngOnChanges runs whenever an @Input value changes (and once initially, before ngOnInit) — the rough equivalent of useEffect with a specific prop in its dependency array. The SimpleChanges argument is an object keyed by which inputs changed, each entry holding previousValue and currentValue — useful when a component needs to react to which input changed and by how much, like recomputing a chart only when its data prop actually changes.

ngOnDestroy — Cleanup Before Removal

import { OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; export class ClockComponent implements OnInit, OnDestroy { private sub?: Subscription; ngOnInit() { this.sub = interval(1000).subscribe(() => console.log('tick')); } ngOnDestroy() { this.sub?.unsubscribe(); // stop the subscription when the component is removed } }

ngOnDestroy runs just before Angular removes the component — the place for cleanup, exactly mirroring the cleanup function returned from a React useEffect. This is where the manual subscriptions flagged in Chapter 11's warning get unsubscribed, timers cleared, and listeners removed. The same rule from React applies: anything that "starts" something ongoing needs a matching "stop" here, or it leaks.

The async pipe and takeUntilDestroyed avoid manual ngOnDestroy
Manually managing a Subscription and unsubscribing in ngOnDestroy is verbose. Two modern alternatives largely remove the need: the async pipe (Chapter 11) cleans up its own subscription automatically, and the takeUntilDestroyed() operator ties an Observable's lifetime to the component's automatically. Prefer those where possible; reach for an explicit ngOnDestroy for non-RxJS cleanup (a manual timer, a third-party library handle).
ngOnChanges only fires for @Input changes — and watch object mutation
ngOnChanges reacts to changes in @Input properties specifically, not to internal state changes. And like React's dependency comparison, it detects a changed input by reference for objects/arrays — mutating an object passed as an input in place (rather than passing a new one) won't be seen as a change, the same immutability concern from React's Fundamentals Chapter 3. Pass a new object/array to trigger ngOnChanges reliably.
Angular hookReact useEffect equivalent
ngOnInituseEffect(() => {...}, []) (mount)
ngOnChangesuseEffect(() => {...}, [someProp])
ngOnDestroyThe cleanup function returned from useEffect

Coding Challenges

Challenge 1

Build a component implementing OnInit that logs a message and fetches/sets some initial data in ngOnInit, confirming (via console) that it runs once after the component is created.

📄 View solution
Challenge 2

Build a child component with an @Input value, implementing OnChanges to log the previousValue and currentValue each time the input changes. Drive it from a parent with a button that changes the input.

📄 View solution
Challenge 3

Build a component that starts an RxJS interval subscription in ngOnInit (logging a tick each second) and properly unsubscribes in ngOnDestroy. Toggle the component's presence with @if in a parent to confirm the ticking stops when it's removed.

📄 View solution

Chapter 12 Quick Reference

  • ngOnInit — runs once after creation; the place for initial setup/data fetching (mount equivalent)
  • Use ngOnInit, not the constructor, for init logic — inputs aren't ready in the constructor
  • ngOnChanges(changes) — runs when an @Input changes; SimpleChanges gives previous/current values
  • ngOnDestroy — runs before removal; the place for cleanup (unsubscribe, clear timers)
  • Implement the matching interface (OnInit/OnChanges/OnDestroy) for type safety
  • The async pipe and takeUntilDestroyed() reduce the need for manual ngOnDestroy cleanup
  • ngOnChanges detects object/array input changes by reference — pass new ones, don't mutate
  • Next chapter: standalone components and signals — Angular's modern direction