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.
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.
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));
["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.
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.