Angular – The Component Lifecycle

July 9, 20263 min readUpdated 8/21/2026

Angular calls a fixed set of methods on your component as it is created, updated and destroyed. The list is worth knowing. What is more useful is knowing how few of them modern Angular still needs — the pizza app has one ngOnInit across thirty components, and no other lifecycle hook at all.

The hooks, in order

ngOnChanges — an input changed. Receives a SimpleChanges bag, fires for every input at once, and runs before ngOnInit.

ngOnInit — once, after the first inputs are set.

ngDoCheck — every change-detection pass. Almost never what you want.

ngAfterContentInit / ngAfterContentChecked — projected content is ready.

ngAfterViewInit / ngAfterViewChecked — the component's own template and children are in the DOM.

ngOnDestroy — being torn down. Historically where every unsubscribe went.

Each is a method you declare; implementing the matching interface — implements OnInit — is optional and worth doing, because a typo in the method name is otherwise silent.

The one that survives: ngOnInit

A component's constructor runs before its inputs are set. So anything that depends on an input cannot go there:

ngOnInit(): void {
  void this.poll();
}

⚠️ This is not a style preference. Reading a required input from the constructor throws NG0950 at runtime, with nothing failing at build time. The input is assigned after construction, and the constructor is simply too early.

There is no React equivalent to this gap, because there is none to have: props are an argument to the function, so they exist before the body runs.

What replaced the rest

ngOnChanges is now computed

readonly isPizza = computed(() => this.product().type === 'PIZZA');
readonly cheapest = computed(() => Math.min(...this.product().sizes.map((s) => s.price)));

An input() is a signal, so anything derived from it can be a computed, which recalculates when that input changes and not when any other does. ngOnChanges fires for all of them and hands you a bag to unpick — strictly worse for the job it was built for.

ngAfterViewInit is now afterNextRender

afterNextRender(() => {
  if (this.appAutofocus()) this.host.nativeElement.focus();
});

afterNextRender runs once after the DOM is written; afterRenderEffect is its reactive sibling, re-running when a signal it read changes — which is what the modal uses to re-focus its close button each time it reopens.

Both are also SSR-safe: they do not run on the server, where there is no DOM. ngAfterViewInit does, which is a common source of "ResizeObserver is not defined" during server rendering.

ngOnDestroy is now DestroyRef

const destroyRef = inject(DestroyRef);

afterNextRender(() => {
  const observer = new ResizeObserver(([entry]) => {
    const next = Math.round(entry.contentRect.width);
    if (next > 0) width.set(next);
  });

  observer.observe(host.nativeElement);
  destroyRef.onDestroy(() => observer.disconnect());
});

This is the real gain, and it is not about syntax. DestroyRef can be injected anywhere there is an injection context — including inside a plain function like this one, which is not a component and has no lifecycle of its own to hook.

That is what lets the whole behaviour be packaged as a function a component calls, with its setup and its teardown sitting next to each other. With ngOnDestroy, the observer would have to be created in one method and disconnected in another, on a component that does not otherwise care, with a field between them holding the reference. Every leak in that pattern comes from the distance between those two lines.

For observables, the same idea has a dedicated operator:

source$.pipe(takeUntilDestroyed()).subscribe(handler);

It unsubscribes when the injection context is destroyed, replacing the destroy$ = new Subject() plus takeUntil(this.destroy$) plus ngOnDestroy ritual that used to appear in every second Angular component.

Effects have their own lifetime

An effect created in an injection context is destroyed with its owner, so most effects need no teardown at all. Where the handle is kept, the cleanup is explicit:

this.destroyRef.onDestroy(() => ref.destroy());

The summary worth remembering

Reach for ngOnInit when you need an input at startup. Reach for afterNextRender when you need the DOM. Reach for DestroyRef to clean up. Everything else the hooks used to do is better done by computed and effect, which is why a modern Angular component usually has none of them.

What is next

Services and dependency injection — the inject() that has been quietly doing the work in every file so far.