Course outline

Angular / Intermediate / Lesson 11

Lifecycle and resource cleanup

Place initialization and teardown at their actual ownership boundaries, avoiding state changes during rendering and work that survives a destroyed view.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish construction, initialized inputs, view readiness, and destruction.
  • Use DestroyRef and explicit cleanup ownership.
  • Test delayed work and teardown without leaking timers or listeners.

Construction does not mean every dependency of the view is ready

Angular constructs a component, assigns inputs, checks its view, and eventually destroys it. ngOnInit runs once after initial input assignment, while ngOnChanges responds to input changes and first runs before ngOnInit during initialization. A view query and a required input have different readiness points. Choose a supported reactive derivation when possible rather than copying values through several hooks. Hooks that run frequently should not hide expensive work. Avoid changing state in the middle of a render traversal merely to force another pass; this can create unstable behavior and development-mode expression-change errors.

Verify cleanup with a real local owner

The complete program creates a local injector, obtains its DestroyRef in a supported context, and registers one cleanup callback. Destroying the injector appends the closing event. This verifies the callback ownership mechanism without a component or timer. The callback is attached to this owner’s lifetime, not to an arbitrary lexical block. A component service may have a different owner than the view that currently uses it, so provider scope matters for cleanup too. Keep setup and teardown close enough that reviewers can see which resource is released and when.

typescript Complete local destruction callback check
import { DestroyRef, Injector, inject, runInInjectionContext } from "@angular/core";
const owner = Injector.create({ providers: [] });
const events: string[] = [];
runInInjectionContext(owner, () => {
  inject(DestroyRef).onDestroy(() => events.push("closed"));
  events.push("created");
});
owner.destroy();
console.log(JSON.stringify(events));
Expected output
["created","closed"]

The event sequence is created then closed. No browser listener, interval, or application is started.

Pass the destruction owner when context is no longer implicit

The contextual component captures DestroyRef during initialization and passes it explicitly to takeUntilDestroyed inside ngOnInit. That hook is not itself a general injection context, so relying on implicit injection there is a different contract. The interval is illustrative and is not started by this course. A configured component test should control time and verify no later updates occur after destruction. Completion of a finite stream can release its own resources, but long-lived subscriptions, listeners, observers, and imperative timers still need an ownership policy. Do not leave cleanup to a page refresh.

typescript Contextual subscription with explicit cleanup owner
import { Component, DestroyRef, OnInit, inject, signal } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { interval } from "rxjs";
@Component({ selector: "app-tick-panel", standalone: true, template: `<p>{{ tick() }}</p>` })
export class TickPanel implements OnInit {
  readonly tick = signal(0);
  private readonly destroyRef = inject(DestroyRef);
  ngOnInit(): void {
    interval(1000).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.tick.set(value));
  }
}

A controlled component fixture should observe ticks only while its owner exists. This fragment is not executed and creates no live timer here.

DOM readiness and asynchronous completion need separate checks

Use documented render callbacks for imperative DOM work that truly needs a completed render, rather than assuming the constructor can measure a rendered element. Render callbacks have injection and platform constraints; browser-only DOM work must not run during server rendering. An async result can also arrive after the component is destroyed or after its input identifies a different entity. Tie subscriptions to lifetime and use an appropriate latest-work policy. A destroyed check can help reject delayed imperative work, but it does not replace correct cancellation and state ownership. Avoid broad timeouts used only to hide an unclear lifecycle assumption.

Test destruction as a normal feature path

Exercise route departure, conditional removal, repeated creation, and cancellation while work is pending. Count listener or subscription teardown in a controlled fixture and ensure a replacement component does not receive the old instance’s result. A local DestroyRef test proves only the registered callback, while a component test proves Angular invokes the intended owner lifecycle. Do not log sensitive payloads to diagnose cleanup. Keep errors visible when a resource cannot be released and define whether that is recoverable. Reliable teardown reduces leaks and stale updates, but it should preserve the user-facing cancellation contract rather than silently discarding committed server work.

Common errors and fixes

inject is called in ngOnInit without a context.

Why it happens
Input readiness was confused with injection-context availability.
How to fix it
Capture dependencies in a supported initializer and pass them explicitly.
Prevent it next time
Test lifecycle and DI assumptions separately.

A removed component still receives timer updates.

Why it happens
The resource outlived its intended owner.
How to fix it
Register teardown or use an owner-bound subscription.
Prevent it next time
Test destroy while work is pending.

A timeout hides an expression-change error.

Why it happens
State mutation occurs at an unsuitable render phase.
How to fix it
Model the dependency in the correct initialization or reactive path.
Prevent it next time
Avoid timing patches without a lifecycle explanation.

Check your understanding

1. Register two cleanup callbacks in the local owner model, then destroy it. What should the test prove without assuming an undocumented callback ordering?

Show worked solution

Verify both required cleanup effects occur exactly as the feature contract requires and no resource remains active. Do not build correctness on an unspecified relative callback order; combine dependent cleanup into one deliberate operation if order matters. The test can compare a set of independent cleanup markers while separately asserting each resource is released.

2. A root service starts an interval used by one short-lived view. Explain why the view’s destruction may not stop the service resource.

Show worked solution

The service belongs to the root injector lifetime, which can outlive that view. Bind the resource to the actual consumer lifetime, move the state to a feature owner when appropriate, or expose a deliberate subscription/cancellation contract. Merely adding cleanup to the root service does not make it run whenever a child view disappears. Test the chosen ownership path.

Key takeaways

  • Match setup and teardown to the same owner.
  • Input readiness, injection context, and DOM readiness differ.
  • Verify destruction with pending work.

Official references