Lesson 10 of 30
ngOnInit and ngOnDestroy still matter — but in a signals-first component, effect() often replaces the hooks you used to reach for in between.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A component isn't just "created, then it exists" — it goes through a sequence of well-defined moments: constructed, its inputs set, its view initialized, its children's views initialized, and eventually destroyed. Angular's classic lifecycle hooks — ngOnInit, ngOnChanges, ngAfterViewInit, ngOnDestroy, and a few others — are methods you implement to run code at those exact moments. This part of Angular hasn't changed with the move to signals; these hooks are still real, still used, and still the right tool for genuinely lifecycle-shaped work like "fetch data once when this component first appears" (ngOnInit) or "clean up a subscription/timer/DOM listener when this component leaves" (ngOnDestroy).
What has changed is how much code used to live in those hooks purely to react to a value changing — and that's shrinking. In the decorator era, ngOnChanges was often the only way to notice "one of my @Input()s changed" and re-derive something from it. With signal inputs, that whole category of hook usage becomes unnecessary: a computed() or an effect() built on top of an input signal reacts to changes on its own, without you writing a lifecycle method to detect the change at all.
effect() specifically runs a function automatically whenever any signal it reads changes — including on initial run. It's not a lifecycle hook in the classical sense (it's not tied to "when the component was constructed"), but in practice it now covers a lot of the ground ngOnChanges and parts of ngOnInit used to cover: "run this side effect whenever this reactive value changes." A newer sibling, afterRenderEffect(), is for effects that specifically need to run after Angular has finished rendering to the DOM — useful for measuring or manipulating actual rendered elements, which effect() alone doesn't guarantee has happened yet.
import { Component, OnInit, OnDestroy, input, effect, signal } from '@angular/core';
@Component({
selector: 'app-product-detail',
standalone: true,
template: `<h2>{{ productId() }}</h2>`,
})
export class ProductDetailComponent implements OnInit, OnDestroy {
productId = input.required<string>();
private pollHandle?: ReturnType<typeof setInterval>;
private viewStartedAt = signal(0);
constructor() {
// runs once immediately, then again every time productId() changes —
// no ngOnChanges needed to detect that the input moved
effect(() => {
console.log('analytics: viewing product', this.productId());
});
}
ngOnInit() {
// genuinely one-time setup unrelated to any particular signal —
// still the right tool for this
this.viewStartedAt.set(Date.now());
this.pollHandle = setInterval(() => this.refreshStock(), 30_000);
}
ngOnDestroy() {
// cleanup still needs an explicit hook — nothing about signals
// automatically tears this down for you
clearInterval(this.pollHandle);
}
private refreshStock() {
/* ... */
}
}
Notice the division of labor: the effect() in the constructor handles "something reactive changed, respond to it" — it doesn't care why productId() changed, whether from the parent rebinding it or anything else, it just reacts. ngOnInit and ngOnDestroy handle things that are genuinely about the component's own lifespan — starting a poll when the component appears, stopping it when the component is torn down — which isn't naturally expressed as "some signal changed." Signals didn't eliminate lifecycle hooks; they eliminated the specific chunk of lifecycle-hook usage that was really just change-detection duct tape.
ngOnDestroy is still mandatory for non-signal cleanup. Timers, addEventListener calls, WebSocket connections, third-party library subscriptions — none of that gets cleaned up automatically just because your component also happens to use signals. effect() has automatic cleanup only for effects it creates itself (via its cleanup callback), not for unrelated resources you set up elsewhere.ngOnChanges still exists and still fires for decorator-based @Input() properties — but if a component has fully migrated to input(), there's usually nothing left for it to tell you that a computed() or effect() on the input signal wouldn't already tell you more directly.effect() runs during change detection, not synchronously the instant a signal changes. Setting a signal inside an event handler and expecting an effect() reading it to have already run by the very next line of that same handler is a common source of confusion — the effect runs on Angular's own schedule, not inline with the .set() call.This mirrors the exact split React developers already navigate: useEffect with a dependency array reacts to state/props changing (Angular's effect()), while genuinely one-time mount/unmount logic still needs its own handling — an empty-dependency-array useEffect standing in for ngOnInit, and its returned cleanup function standing in for ngOnDestroy. Both frameworks arrived at the same conclusion: reactive derivation and lifecycle-bound setup/teardown are different problems that shouldn't be forced through one mechanism.