In Angular, what does multi: true do on a provider, and why can a component-level multi provider hide contributions registered at the root?
answer
- one token, many contributions
- injector returns an array
- arrays do not merge across injectors
- mixing forms is rejected
basics
~20 smulti: 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 sWithout `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 linesimport { 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
Know that multi: true turns a token into an array of contributions and that injecting it returns that array.
Explain registration order, that each recipe can contribute one entry, and why the class shorthand cannot be multi.
Diagnose disappearing plugins caused by a closer injector's multi array shadowing the root, and show how to extend a parent's array deliberately.
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