skip to content

In an Angular wizard component that receives its steps through <ng-content>, what changes when its form-state service moves from providers to viewProviders?

level: middleimportance: should knowfreq 45%

answer

  1. who can see it, not how many
  2. own view vs projected content
  3. content resolves where it is declared
  4. a root fallback can hide the break

basics

~20 s

viewProviders are visible only inside the component's own template, so projected step components can no longer see the wizard's form state: they get NG0201, or silently the root instance if the service is also root-provided.

solid answer

~40 s

`providers` on a component are visible to the component, everything in its template, and content projected into it. `viewProviders` are visible only to the component and its own view. Projected content is resolved from the template where it is **declared** — the parent that wrote `<app-wizard><app-address-step/></app-wizard>` — and its walk passes the wizard's host element, where only `providers` are exposed. So after the move, the wizard's own progress bar still sees the state, but projected steps fall through to environment injectors: NG0201 if the service is `@Service({autoProvided: false})` or plain `@Injectable()`, or a different, shared root instance if it is `@Service()` / `providedIn: 'root'` — the silent bug. Use `viewProviders` for internals you want hidden from consumers' content, `providers` when projected children must share the state.

code

ts · 32 lines
ts
import {Component, Service, inject, signal} from '@angular/core';

@Service({autoProvided: false}) // must be provided explicitly: no silent root fallback
export class WizardState {
  readonly step = signal(0);
  readonly answers = signal<Record<string, string>>({});
}

@Component({selector: 'app-wizard-progress', template: `Step {{ state.step() + 1 }}`})
export class WizardProgress {
  state = inject(WizardState); // in Wizard's own view: works with either array
}

@Component({
  selector: 'app-wizard',
  imports: [WizardProgress],
  viewProviders: [WizardState], // change to providers: [WizardState] so steps can see it
  template: `<app-wizard-progress /><ng-content />`,
})
export class Wizard {}

@Component({selector: 'app-address-step', template: `<p>address</p>`})
export class AddressStep {
  state = inject(WizardState); // projected into <app-wizard>: NG0201 with viewProviders
}

@Component({
  selector: 'app-checkout-page',
  imports: [Wizard, AddressStep],
  template: `<app-wizard><app-address-step /></app-wizard>`,
})
export class CheckoutPage {}

go deeper

for a junior

Know that components have both providers and viewProviders, and that the difference is only whether content passed in with ng-content can see the service.

for a middle

Explain that projected content is resolved from its declaring template, so its walk passes the host element, where only providers are exposed.

for a senior

Spot the silent failure: a root-provided service that projected children fall back to, and choose non-auto-provided services so the mistake fails loudly.

for a principal

For a component library, decide which services are public contract via providers and which stay private via viewProviders, and document the difference for consumers.

## Two ways to provide on a component A `@Component` decorator accepts two provider arrays, both of which configure the **element injector** of the component's host element: | | `providers` | `viewProviders` | |---|---|---| | Visible to the component class | Yes | Yes | | Visible to its own template (its **view**) | Yes | Yes | | Visible to content projected with `<ng-content>` | Yes | **No** | | Available on `@Directive` | Yes | No — components only | | Instances per host | One | One | Both create one instance per component instance, and both are destroyed with the host. The **only** difference is visibility to projected content. ## Why projected content cannot see viewProviders **Content projection** lets a parent write children between a component's tags: ```html <!-- checkout-page.html --> <app-wizard> <app-address-step /> <app-payment-step /> </app-wizard> ``` The step components are rendered inside the wizard's `<ng-content>` slot, but they are **declared** in the checkout page's template. Angular resolves injection against where content is declared, not where it renders. In the checkout page's template, `<app-address-step>` is a child of the `<app-wizard>` element, so its element-injector walk goes: 1. its own element injector; 2. the `<app-wizard>` host element — where only the wizard's `providers` are exposed; 3. the checkout page's host and further ancestors; 4. then the environment injectors. The wizard's `viewProviders` belong to its **internal view**, which the step is not part of, so they are skipped. ## What actually breaks after the move The result depends on whether there is any other provider on the path: - **No other provider** (the service is `@Service({autoProvided: false})` or `@Injectable()` without `providedIn`): each projected step throws `NG0201: No provider found for \`WizardState\``. Loud, easy to find. - **A root fallback exists** (the service is `@Service()` or `@Injectable({providedIn: 'root'})`): each step silently receives the **root singleton**, while the wizard and its own template use the per-wizard instance. The steps write answers into a store nobody reads, and two wizards on one page share steps' state. This is the dangerous case, because nothing errors. - **Elements in the wizard's own template** — a progress bar, the Next and Back buttons — keep working, because they live in its view. ## When to choose each Use `providers` when: - projected children are meant to collaborate with the host — tabs and tab panels, a wizard and its steps, a form group and its controls; - directives on the same element or content children must share the instance. Use `viewProviders` when: - the service is an **implementation detail** of the component's template, and you do not want consumer content to reach it — or to accidentally shadow a service the consumer's content expects from further up; - you are writing a reusable library component and want to keep its internals private. ## A safer shape for the wizard For a wizard whose steps arrive by projection, the per-instance state belongs in `providers`, declared as a non-auto-provided service so a missing provider fails loudly instead of silently falling back to a root instance. If you need a private helper in the wizard's template as well, put that one in `viewProviders`: the two arrays can be used together on the same component. ## Related effects to keep in mind - **Two wizards on one page** each get their own `WizardState` with either array — the arrays differ in visibility, never in instance count. - **The wizard class itself** can inject the service from either array, so moving it never breaks the host component — only the consumers' projected content, which is why the regression often goes unnoticed in the wizard's own tests. - **Tests** that render the wizard with projected steps are the quickest way to catch the difference: a step that reads the state in a test fixture fails immediately under `viewProviders` with a non-auto-provided service. - **Shadowing works both ways**: a service in `providers` also becomes the closest match for everything the consumer projects — including components that expected a different instance from further up the tree. That is the case `viewProviders` exists to prevent.

  • If WizardState were decorated with @Service() and the wizard used viewProviders, what would the projected steps receive?
    The root singleton. The steps' walk skips the wizard's viewProviders, finds nothing else on the element path, and falls through to the root environment injector, where `@Service()` auto-provides the class. They get a shared instance separate from the wizard's own, with no error to warn you.
  • How can a component deliberately give projected template content access to its viewProviders?
    Take the content as an `<ng-template>` rather than plain projected nodes, query it, and render it with `NgTemplateOutlet` while passing `ngTemplateOutletInjector` set to the component's own injector, obtained with `inject(Injector)` in the component class. The template is then created at the outlet with that injector, so the viewProviders resolve.

saying these in an interview costs you the question

  • viewProviders create one instance per child view
  • viewProviders make a service invisible to the component's own template
  • Projected content resolves from where it renders
  • Directives can declare viewProviders too
  • Switching to viewProviders always produces a loud error