Angular PrimerAngular 22 · 2022 → 2026

Dependency injection

`inject()` in a field initialiser instead of a constructor parameter, `@Service()` instead of the `providedIn` incantation, and providers you can compose as plain data.

inject() instead of constructor parameters

The old form still compiles, but the new one is shorter and — more importantly — works in more places:

2022 — constructor injection

@Injectable({ providedIn: 'root' }) export class Basket { constructor(private http: HttpClient, private router: Router) {} }

2026 — inject(), and @Service()

@Service() export class Basket { private readonly http = inject(HttpClient); private readonly router = inject(Router); }

inject() is not just syntax. Because it is a function call rather than a constructor signature, it works in field initialisers, in computed factories, in functional guards and resolvers, in an InjectionToken factory, and in provideAppInitializer — none of which can take constructor parameters. That is the real reason the framework moved.

inject() only works in an injection context: a field initialiser, a constructor, a factory, or a function the framework calls during creation. Calling it inside a setTimeout, a click handler, or an await continuation throws "inject() must be called from an injection context". The fix is usually runInInjectionContext or — better — capture the dependency in a field and use it later.

@Service() and scope

@Injectable({ providedIn: 'root' }) said two things at once: this class can be injected, and one instance lives at the root. v22 splits the intent:

2026 — the two spellings
@Service()                                  // root-provided, one instance app-wide
export class Theme {
  readonly mode = signal<'light' | 'dark'>('light');
}

@Injectable({ providedIn: 'root' })         // still valid, still common
export class Legacy {}

@Service() sets autoProvided: true by default, which is exactly the old providedIn: 'root'. For anything with a narrower life — a component-level store, a per-route cache — declare it in the providers of the component or the route instead:

2026 — a scoped provider
@Component({
  selector: 'app-basket-panel',
  providers: [BasketStore],          // one instance per component instance
  template: `...`,
})
export class BasketPanel {}

The rule of thumb has not changed since 2022: root for stateless or genuinely global, component or route for anything with a lifetime shorter than the app.

InjectionToken

A token is how you inject something that is not a class — a string, a config object, a function:

2026 — a typed token
export const API_URL = new InjectionToken<string>('API_URL');

// provided once, in app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [{ provide: API_URL, useValue: '/api' }],
};

// injected anywhere
private readonly apiUrl = inject(API_URL);

useValue for a literal, useFactory for a computed value, useExisting to alias another token, useClass to pick an implementation. That last one is the seam you want for tests and for swapping a real backend for a fake:

2026 — swapping an implementation, in a test
TestBed.configureTestingModule({
  providers: [{ provide: WeatherApi, useClass: FakeWeatherApi }],
});

InjectionToken + inject() scopes

The host provides a signal under a token, one child injects it, and a second child overrides it for its own subtree. Writing from the host updates the first child and leaves the override alone.

the host reads the token: provided by the demo host

a child injects it: provided by the demo host

a child that provides its own: provided by this component’s own provider

Providers as data

The provider array is a value, so it composes. This is the pattern that replaced wide, deeply nested modules:

2026 — feature providers, spread where needed
export const WEATHER_PROVIDERS: Provider[] = [
  { provide: API_URL, useValue: '/api/weather' },
  WeatherStore,
];

// app.config.ts
providers: [
  provideHttpClient(withFetch()),
  ...WEATHER_PROVIDERS,
]

Startup work: provideAppInitializer

APP_INITIALIZER as a multi-provider token is gone; the modern form is a plain function that runs before the app renders, and it may return a promise or an observable:

2026 — async work before bootstrap
export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => {
      const config = inject(ConfigService);
      return config.load();          // the app waits for this
    }),
  ],
};

It is the right place for "the app cannot render without this" — a feature flag payload, a locale bundle, an auth handshake. It is the wrong place for data that a page can show a skeleton for; that belongs in a resource().

For a dependency that arrives asynchronously and is only needed sometimes, v22 adds injectAsync():

2026 — awaiting a dependency
readonly heavy = injectAsync(() => import('./heavy-analyzer').then((m) => new m.Analyzer()));

The awaited factory runs inside an injection context, so anything it injects is resolved normally — useful for lazily loaded WASM, a large parser, or a config file fetched at first use.

Hierarchical injectors, briefly

Injectors form a tree: root, then the route that declared providers, then the component. A request walks up from the requesting injector until it finds a provider. Two consequences worth keeping in mind:

  • A route-level provider is a new instance per route activation, which is often what you want for a page-scoped store.
  • The same token can be provided at several levels, and the nearest one wins — that is the mechanism behind "provide a fake in this test, the real thing everywhere else".

What to take away

inject() made dependencies a value you read rather than a constructor you are stuck with; @Service() made the common case one line; providers as data made composition possible. Next, the two places this shows up most: Routing, where providers and guards became functions, and HTTP & async data, where the client is provided the same way.