In Angular, what does a route's providers array create, which code can inject from it, and how long do those instances live?
answer
- a new EnvironmentInjector per route
- route, its children, guards, resolvers, loaders
- created on first match or preload
- not destroyed on leaving, by default
- withAutoCleanupInjectors
basics
~20 sA route's providers array makes the router create an EnvironmentInjector for that route and its children; their components, guards, resolvers and loaders can inject from it. It lives on after you leave unless withAutoCleanupInjectors is enabled.
solid answer
~40 sAdding `providers: [ReportsStore]` to the `reports` route makes the router create a child **`EnvironmentInjector`** for that route, parented to the injector above it. Everything under that route resolves from it: routed components and their children, guards, resolvers, and the `loadComponent`/`loadChildren` functions of the route and its descendants. It is created the first time the route is matched (or when the preloader visits it), so a service listed only there sits in that route's lazy chunk when the route config itself is lazily loaded. A `ReportsStore` provided there is a separate instance from any root one. By default the injector is **not destroyed** when the user leaves the route, so its services keep their state; since 22.2, `withAutoCleanupInjectors()` (stable) destroys unused route injectors.
code
ts · 19 linesimport { ApplicationConfig, InjectionToken } from '@angular/core';
import { provideRouter, Routes, withAutoCleanupInjectors } from '@angular/router';
import { ReportsStore } from './reports/reports-store';
export const REPORTS_PAGE_SIZE = new InjectionToken<number>('REPORTS_PAGE_SIZE');
const routes: Routes = [
{
path: 'reports',
// one EnvironmentInjector for /reports and everything under it
providers: [ReportsStore, { provide: REPORTS_PAGE_SIZE, useValue: 50 }],
loadChildren: () => import('./reports/reports.routes'),
},
];
export const appConfig: ApplicationConfig = {
// without this, ReportsStore survives after the user leaves /reports
providers: [provideRouter(routes, withAutoCleanupInjectors())],
};go deeper
Recall that routes can list providers, and that those services are available to the components under that route.
Explain that the router creates an EnvironmentInjector for the route, that guards, resolvers and loaders use it, and when it is created.
Anticipate lifetime bugs: services surviving after leaving a route, duplicate instances shadowing root, and when to enable withAutoCleanupInjectors.
Decide the scoping policy for feature state across route, component and root injectors, and the memory trade-off of long-lived route injectors.
## What the router builds A route definition can carry its own `providers` array. When the router first needs that route, it calls `createEnvironmentInjector(route.providers, parentInjector)` and stores the result on the route. So each route with providers gets its own **`EnvironmentInjector`**, a node in the same tree as the root injector, sitting between the parent route's injector (or the root) and everything below it. ```ts { path: 'reports', providers: [ReportsStore, { provide: REPORTS_PAGE_SIZE, useValue: 50 }], loadChildren: () => import('./reports/reports.routes'), } ``` The array accepts both ordinary providers and `EnvironmentProviders` (the result of `provideX()` functions), which is how a feature area can call something like `provideHttpClient(withInterceptors([...]))` for just its own routes. ## Who can inject from it - components activated by this route and by its child routes, and their descendants; - the route's and its children's **guards** and **resolvers**, which run in the route's injection context; - the `loadComponent` and `loadChildren` functions of the route and its descendants, which also run in that context; - services created from this injector. Code outside the subtree, such as the root `App` component or a sibling `/settings` route, cannot see these providers; it resolves from root. ## When it is created | Moment | What happens | |---|---| | first navigation that matches the route | the router creates the injector while recognising the URL | | preloading pass that reaches the route | the preloader creates the injector so the loaders can run in it | | later visits | the stored injector is reused | This ties provider creation to the route, not to application start. A service whose class is referenced only from a lazily loaded routes file ends up in that lazy chunk, which is one reason route providers pair naturally with `loadChildren`. ## How long the instances live The default surprises people: leaving `/reports` does **not** destroy the route's injector. The `ReportsStore` instance and its state survive, and returning to `/reports` gets the same instance. That is often desired (a cache that outlives the page), but it also means subscriptions or timers started by those services keep running. Angular 22.2 made **`withAutoCleanupInjectors()`** stable (it replaces the deprecated `withExperimentalAutoCleanupInjectors()`). With it, after navigations the router destroys the environment injectors of routes that are no longer active, as long as the `RouteReuseStrategy` agrees via `shouldDestroyInjector(route)`; the built-in strategy's implementation returns `true`. Destroying the injector runs `ngOnDestroy` on its services and `DestroyRef` callbacks registered in it. Routes kept alive by a custom reuse strategy are not destroyed if the strategy reports their stored handles. ## Providers for a lazily loaded area A lazily loaded `Routes` array does not get an injector of its own; only a lazily loaded NgModule did. To scope services to a whole lazy area, either put `providers` on the parent route that has the `loadChildren` (as above), or wrap the lazy file's routes in a componentless parent: ```ts // reports.routes.ts export default [ { path: '', providers: [ReportsStore], children: [ { path: '', component: ReportsHome }, { path: ':id', component: ReportDetail }, ] }, ] satisfies Routes; ``` The second form keeps the providers next to the code that uses them, so the whole scope lives in the lazy chunk and the app routes file does not need to import `ReportsStore`. ## Contrast with older patterns 1. **NgModule `loadChildren`**: a lazily loaded NgModule got its own module injector; route `providers` are the standalone replacement and do not need a module. 2. **Component `providers`**: a service listed on a component gets a fresh instance per component instance, living exactly as long as that component, in the element injector tree. Route providers instead give one instance for the whole route subtree and live in the environment injector tree. 3. **`providedIn: 'root'`**: one app-wide instance. Providing the same class on a route as well creates a second, route-scoped instance that shadows the root one for that subtree, a common source of "why do I have two stores" bugs.
- Can a component rendered by the root App component inject ReportsStore when ReportsStore is provided only on the reports route?No. The root component resolves from its element injectors and then the root environment injector; the reports route's injector is a child below it, so the lookup never reaches it. Unless `ReportsStore` is also `providedIn: 'root'`, the lookup fails with a missing-provider error.
- What changes if the route injector is destroyed by withAutoCleanupInjectors?Services created in it are destroyed: their `ngOnDestroy` runs and `DestroyRef.onDestroy` callbacks fire, so subscriptions and timers can be cleaned up. The next visit to `/reports` creates a fresh injector and fresh instances, so any in-memory cache in `ReportsStore` starts empty.
saying these in an interview costs you the question
- Route providers create a new instance on every navigation to the route
- Leaving the route always destroys its providers
- Route providers are visible to the whole application
- Guards and resolvers cannot inject route-level providers
- Route providers only work with NgModule-based loadChildren