skip to content

In Angular, what does multi: true do on a provider, and why can a component-level multi provider hide contributions registered at the root?

level: seniorimportance: should knowfreq 40%

answer

  1. one token, many contributions
  2. injector returns an array
  3. arrays do not merge across injectors
  4. mixing forms is rejected

basics

~20 s

multi: true makes a token collect every contribution registered in one injector into an array. Arrays do not merge across injectors: a component that adds its own multi entry creates a new array, hiding the root's entries from its descendants.

solid answer

~40 s

Without `multi`, a second provider for a token replaces the first. With `multi: true`, each provider adds one entry and `inject(TOKEN)` returns an array of all entries in registration order — the basis of plugin points such as a list of payment methods. Each entry can use any recipe (`useClass`, `useValue`, `useFactory`, `useExisting`), but the bare class shorthand cannot be multi. The trap is scope: the array belongs to the injector where the entries are registered. When a component registers its own `multi` entry for the same token, its injector builds a fresh array with only its own entries, and lookups from its subtree stop there, so the root's contributions vanish for that subtree. In an environment injector, mixing `multi` and regular providers for one token throws in development mode.

code

ts · 24 lines
ts
import { Component, InjectionToken, inject } from '@angular/core';

export interface PaymentMethod {
  id: string;
  label: string;
}

export const PAYMENT_METHODS = new InjectionToken<PaymentMethod[]>('PAYMENT_METHODS');

// Registered in bootstrapApplication providers:
export const paymentMethodProviders = [
  { provide: PAYMENT_METHODS, useValue: { id: 'card', label: 'Card' }, multi: true },
  { provide: PAYMENT_METHODS, useValue: { id: 'invoice', label: 'Invoice' }, multi: true },
];

@Component({
  selector: 'app-gift-checkout',
  // Creates a NEW array in this component's injector: descendants see only 'gift'.
  providers: [{ provide: PAYMENT_METHODS, useValue: { id: 'gift', label: 'Gift card' }, multi: true }],
  template: `@for (m of methods; track m.id) { <label>{{ m.label }}</label> }`,
})
export class GiftCheckout {
  protected readonly methods = inject(PAYMENT_METHODS); // [{ id: 'gift', ... }] only
}

go deeper

for a junior

Know that multi: true turns a token into an array of contributions and that injecting it returns that array.

for a middle

Explain registration order, that each recipe can contribute one entry, and why the class shorthand cannot be multi.

for a senior

Diagnose disappearing plugins caused by a closer injector's multi array shadowing the root, and show how to extend a parent's array deliberately.

for a principal

Treat multi tokens as public extension points: typed arrays, explicit ordering, small descriptors, and a clear owner for each token.

## What `multi: true` changes Normally an **injector** holds one value per **token**: registering a second provider for the same token simply replaces the first. Adding `multi: true` changes the contract. Every provider flagged `multi` contributes **one entry**, and injecting the token returns an **array** of all entries registered in that injector, in the order they were registered. This is Angular's built-in plugin mechanism. A checkout that supports several payment methods can declare one extension point and let features contribute to it: ```ts export const PAYMENT_METHODS = new InjectionToken<PaymentMethod[]>('PAYMENT_METHODS'); providers: [ { provide: PAYMENT_METHODS, useClass: CardMethod, multi: true }, { provide: PAYMENT_METHODS, useClass: InvoiceMethod, multi: true }, { provide: PAYMENT_METHODS, useValue: sandboxMethod, multi: true }, ] ``` `inject(PAYMENT_METHODS)` then yields `[CardMethod instance, InvoiceMethod instance, sandboxMethod]`. ## Rules worth knowing - Every recipe can be multi: `useClass`, `useValue`, `useFactory` and `useExisting` each add one entry, built by that recipe. - The **bare class shorthand** (`providers: [CardMethod]`) cannot be multi — there is no object to carry the flag, and its token would be the class itself. - Each entry is resolved once and cached with the array; the array is built on the first request. - Order is registration order, so do not rely on it across independently written feature providers unless you control that order. - In an **environment injector** (the root, a route's injector, one created from bootstrap providers), registering the same token both with and without `multi` throws `Cannot mix multi providers and regular providers` — a development-mode check; production builds skip it. ## Why a component can hide the root's entries Lookup in Angular's hierarchical DI stops at the **first injector that has a provider for the token**. A multi array is a single value owned by one injector; it is not stitched together with arrays in ancestor injectors. So: | Where entries are registered | What a component's descendant injects | |---|---| | Only at the root | the root's array | | At the root and in a component's `providers` | only the component's array | | Root and a lazy route's injector | only the route injector's array, for code under that route | A team that adds `{ provide: PAYMENT_METHODS, useClass: GiftCardMethod, multi: true }` to one checkout component expecting "root methods plus gift card" gets **only** the gift card inside that component's subtree. Nothing throws; methods simply disappear. When a subtree genuinely needs "parent's entries plus mine", register a regular (non-multi) provider for the token with a factory that reads the parent's array — using a lookup modifier that skips the current injector — and returns a new array with the extra entry. ## Diagnosing multi-provider bugs 1. **An entry is missing in one screen only.** Look for a closer injector — a component, directive or route — that registers the same token; its array shadows the ancestors'. 2. **The value is an object, not an array.** Somewhere the token is registered without `multi`. Environment injectors reject that mix in development builds; element injectors do not run the check, so the outcome there depends on registration order. 3. **An entry appears twice.** The same provider array was spread into two places of one injector, for example a provider helper called twice. 4. **Order-dependent behaviour.** A consumer takes `methods[0]` as the default; any feature added earlier in the provider list changes it. Make ordering explicit with a field on the entry. ## Consuming the array safely The array a multi token returns is the injector's cached value, shared by every consumer of that injector. Treat it as read-only: sorting it in place or pushing into it changes what every other consumer sees, in whatever order they happened to run. Copy before reordering (`[...methods].sort(...)`), look entries up by an explicit `id` rather than by index, and handle the empty case, because a subtree can legitimately end up with an array that has no entries a given consumer expected. When optional contributors may be absent, inject the token optionally and default to an empty array. ## Design advice - Type the token as an array, `InjectionToken<PaymentMethod[]>`, so consumers see what they receive. - Keep entries small and declarative (a descriptor object or a lightweight class) so a plugin list does not force every feature's code into the initial bundle. - Prefer one extension point per concern; a token that mixes unrelated contributions becomes a hidden global registry.

  • How would you let one feature area extend the root's payment methods instead of replacing them?
    Register a regular provider for the token in that area with a factory that injects the parent injector's array, skipping the current injector, and returns a new array with the extra method appended. The subtree then sees root entries plus its own, and the root array is never mutated.
  • Why type a multi token as InjectionToken<PaymentMethod[]> rather than InjectionToken<PaymentMethod>?
    Because `inject()` returns what the token's type parameter says, and a multi token always resolves to an array. Typing it as the array makes consumers iterate correctly and stops code that treats the result as a single object from compiling.

saying these in an interview costs you the question

  • Multi arrays from parent and child injectors are merged automatically
  • Without multi, a second provider for the token is appended as another value
  • The class shorthand providers: [X] can be marked multi
  • A multi token returns only the last registered entry
  • Mixing multi and regular providers for one token silently works in every build