Angular 22.2 route injector cleanup can dispose a route-provided service after navigation leaves its route. Add withAutoCleanupInjectors() to the router, then verify the service’s lifetime with a navigation test. A parameter change on the same route is a different case: Angular’s default reuse keeps that route injector alive. The distinction matters if a route service owns a subscription, timer, or other resource.
Table of Contents
Configure Angular 22.2 route injector cleanup
The router feature is an opt-in for route injectors, not a switch that destroys every service in the application. Put it beside the route configuration in provideRouter. This example uses Angular 22.2.0 and a standalone application.
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withAutoCleanupInjectors } from '@angular/router';
import { routes } from './app';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withAutoCleanupInjectors()),
],
};
Registering a service in providedIn: 'root' would test an application-wide provider instead. To observe the cleanup feature, the service must be supplied by a route whose injector can become unused.
Provide a service on the route
The minimal proof counts construction and destruction. In a real service, ngOnDestroy would release resources the service owns. The page injects the service so navigation to /reports/1 actually creates it; merely declaring a route provider does not prove that it was instantiated.
import { Component, Injectable, OnDestroy, inject } from '@angular/core';
import { Routes } from '@angular/router';
@Injectable()
export class RouteScopedService implements OnDestroy {
static created = 0;
static destroyed = 0;
constructor() {
RouteScopedService.created++;
}
ngOnDestroy(): void {
RouteScopedService.destroyed++;
}
}
@Component({ template: '<p>Reports route active</p>' })
export class ReportsPage {
private readonly routeService = inject(RouteScopedService);
}
@Component({ template: '<p>Home route active</p>' })
export class HomePage {}
export const routes: Routes = [
{ path: '', component: HomePage },
{
path: 'reports/:id',
component: ReportsPage,
providers: [RouteScopedService],
},
];
The counters are demonstration instrumentation, not a pattern for application state. The important boundary is providers: [RouteScopedService] on the route. Keep actual cleanup logic idempotent and confined to resources owned by that provider; a route transition may not be the right lifetime for a shared cache.
Test navigation and parameter reuse
Run the complete example with npm ci and npm run check. Its Angular 22.2.0 production build and three local Vitest tests passed on 28 September 2026. The assertions below are the core behavioral check: leaving the reports route destroys its service once, while changing only the :id on the same route does not. Each test resets the counters and configures a fresh test module.
beforeEach(() => {
RouteScopedService.created = 0;
RouteScopedService.destroyed = 0;
TestBed.configureTestingModule({
imports: [App],
providers: [provideRouter(routes, withAutoCleanupInjectors())],
});
});
it('destroys a route provider after navigating away', async () => {
const fixture = TestBed.createComponent(App);
const router = TestBed.inject(Router);
await router.navigateByUrl('/reports/1');
fixture.detectChanges();
expect(RouteScopedService.created).toBe(1);
expect(RouteScopedService.destroyed).toBe(0);
await router.navigateByUrl('/');
fixture.detectChanges();
expect(RouteScopedService.destroyed).toBe(1);
});
it('retains the injector for a parameter-only change', async () => {
const fixture = TestBed.createComponent(App);
const router = TestBed.inject(Router);
await router.navigateByUrl('/reports/1');
fixture.detectChanges();
await router.navigateByUrl('/reports/2');
fixture.detectChanges();
expect(RouteScopedService.created).toBe(1);
expect(RouteScopedService.destroyed).toBe(0);
});
The third test removes withAutoCleanupInjectors() and expects the route provider to remain after leaving. That contrast detects a test that would pass for some unrelated destruction behavior. The tests show lifecycle callbacks for this fixture; they do not measure heap usage or promise that every retained object in an application is collected.
Production limits and custom reuse strategies
Changing /reports/1 to /reports/2 keeps the same route configuration under the default reuse strategy. A service that must refresh when the parameter changes should react to route parameters rather than rely on injector destruction. This is a distinct requirement from releasing the service when the route is left.
Review any custom RouteReuseStrategy before enabling cleanup. Angular’s guide says a strategy that does not extend BaseRouteReuseStrategy needs shouldDestroyInjector; a strategy that stores detached handles also needs retrieveStoredRouteHandles so the router can see them. Do not copy the default-strategy test result to a custom strategy without adding a test for the routes and handles it preserves. The local example does not exercise a custom strategy.
Adopt the feature in a small route first, run the navigation test in your supported Angular version, and inspect services whose state intentionally outlives a route. If cleanup changes that state unexpectedly, move only genuinely shared state to a longer-lived provider or adjust your route design; disabling cleanup globally to hide one ownership mistake leaves the route lifetime unclear.
References
- Angular API: withAutoCleanupInjectors.
- Angular guide: customizing route behavior.
- Angular issue #70101: route injector lifecycle when parameters change.
Demo source code: View the complete runnable example on GitHub.
Found this useful? Support more practical developer content.