An Angular accordion item using inject(Accordion, {host: true}) works inside <app-accordion> but throws NG0201 once moved into a wrapper component's template — why, and how do you fix it?
answer
- boundary is the declaring template
- wrapper becomes the new boundary
- only viewProviders at the boundary
- re-expose through the wrapper
- the forms ControlContainer trick
basics
~20 shost limits the lookup to the template declaring the item. In a wrapper's template the boundary becomes the wrapper, exposing only itself and its viewProviders, so the outer accordion is unreachable. Drop host, or re-expose it via viewProviders.
solid answer
~40 s`host: true` lets the lookup climb element injectors only within the template where the requester is **declared**, stopping at the host of the component that owns that template, and it never falls back to environment injectors. Declared directly in the page as `<app-accordion><app-accordion-item/></app-accordion>`, the item's walk passes the `<app-accordion>` element inside the page template and finds the component. Moved into `FaqGroup`'s template, the boundary becomes `<app-faq-group>`: there only `FaqGroup` itself and its `viewProviders` are visible, the accordion outside is never reached, and the lookup throws NG0201 (or yields `null` with `optional`). Fixes: remove `host` unless you really want template-scoped isolation; or have the wrapper re-expose the parent explicitly with `viewProviders: [{provide: ACCORDION, useExisting: Accordion}]` against an abstract token, which is exactly how Angular forms passes `ControlContainer` into child components.
code
ts · 30 linesimport {Component, InjectionToken, forwardRef, inject} from '@angular/core';
export interface AccordionApi {
toggle(id: string): void;
}
export const ACCORDION = new InjectionToken<AccordionApi>('ACCORDION');
@Component({
selector: 'app-accordion',
providers: [{provide: ACCORDION, useExisting: forwardRef(() => Accordion)}],
template: `<ng-content />`,
})
export class Accordion implements AccordionApi {
toggle(id: string) {}
}
@Component({selector: 'app-accordion-item', template: `<ng-content />`})
export class AccordionItem {
// template-scoped on purpose: only the declaring template's accordion counts
protected accordion = inject(ACCORDION, {host: true});
}
@Component({
selector: 'app-faq-group',
imports: [AccordionItem],
// opt in: re-expose the enclosing accordion at this boundary
viewProviders: [{provide: ACCORDION, useExisting: Accordion}],
template: `<app-accordion-item>Billing</app-accordion-item>`,
})
export class FaqGroup {}go deeper
Recall that host stops the lookup at a component boundary and does not fall back to root-provided services.
Explain that the boundary is the template where the requester is declared, and that only the boundary component and its viewProviders are visible there.
Diagnose a host-related NG0201 after a refactor by comparing declaring templates, and fix it by dropping host or re-exposing through viewProviders, as Angular forms does with ControlContainer.
Judge whether template-scoped lookups belong in a shared component library at all, given how refactors that extract wrappers silently change what they can see.
## What `host` really bounds `host` is usually described as "stop at the host component". The precise rule, as implemented in Angular's node-injector lookup, is: 1. Take the **template in which the requester is declared**. Its owner is the *declaring component*. 2. Walk element injectors upward from the requester, within that template. 3. The last stop is the **declaring component's host element**. There Angular applies a special case: only the declaring component **instance** and its **`viewProviders`** are visible — not its `providers`, and not other directives on that element. 4. Never consult environment injectors. So the boundary is defined by **where the code is written**, not by the DOM or by which component renders the content. ## Why it worked in the page ```html <!-- faq-page.html (template of FaqPage) --> <app-accordion> <app-accordion-item title="Billing" /> </app-accordion> ``` The item is declared in `FaqPage`'s template. Its walk: its own element, then `<app-accordion>` — an **intermediate** element in that template, where the `Accordion` component is found — done. The boundary (`FaqPage`'s host) is never reached. ## Why it broke in the wrapper A refactor extracts a group of items into a reusable component: ```html <!-- faq-page.html --> <app-accordion> <app-faq-group /> </app-accordion> <!-- faq-group.html (template of FaqGroup) --> <app-accordion-item title="Billing" /> <app-accordion-item title="Shipping" /> ``` Now each item is declared in `FaqGroup`'s template. Its walk: its own element, then `<app-faq-group>` — the **boundary**, where only `FaqGroup` and its `viewProviders` count. `Accordion` is not among them, the walk stops, environment injectors are skipped, and the lookup throws NG0201 (`No provider for Accordion found in NodeInjector`). Nothing about the accordion changed; only the **declaring template** did. ## Fixes | Fix | How | When it fits | |---|---|---| | Drop `host` | `inject(Accordion)` | Items should find the nearest accordion anywhere above them — the usual case | | Re-expose through the wrapper | `viewProviders: [{provide: ACCORDION, useExisting: Accordion}]` on `FaqGroup`, items inject `ACCORDION` with `host` | You want template-scoped isolation, and wrappers opt in explicitly | | Pass it in | an input on the item, set by the wrapper | The relationship should be visible in templates rather than implicit | The re-exposure works because: - `viewProviders` of the boundary component **are** visible to a `host` lookup from its template; - `useExisting: Accordion` resolves `Accordion` from `FaqGroup`'s own position in the tree, where the enclosing `<app-accordion>` is an ancestor; - a **separate token** (`ACCORDION`, an `InjectionToken` or abstract class that `Accordion` also provides with `useExisting`) avoids a provider that aliases a token to itself. ## The same pattern in Angular itself Angular's forms directives look up their parent `ControlContainer` with `host` (and `skipSelf` for groups). A child component containing `ngModel` inputs therefore cannot register with a `<form>` in its parent — Angular forms even ships a warning saying "@Host() stops injection at the component boundary". The documented remedy is the one above: ```ts viewProviders: [{provide: ControlContainer, useExisting: NgForm}] ``` Knowing this example is a strong signal in an interview: it shows you understand that `host` is about **templates and viewProviders**, not about "the nearest parent component". ## Why `host` exists at all `host` gives directive authors **template-scoped encapsulation**: a directive that only makes sense together with a partner declared in the same template — a form control and its group, an item and its list — can refuse to bind to a partner that happens to sit further up in some other component. That strictness is valuable in a framework-level package such as forms, where silently registering with the wrong form would be worse than not registering. In application components it is usually more trouble than it is worth, because extracting markup into a wrapper component is a routine refactor, and `host` makes that refactor change behaviour. ## Diagnosis checklist - The message says `found in NodeInjector` — the element walk ended, and no environment fallback happened: suspect `host` or `self`. - Compare where the requester is **declared** before and after the change, not where it renders. - Check whether the boundary component provides the token in `providers` (invisible to `host` from its view) or `viewProviders` (visible).
- Why would listing the alias in FaqGroup's providers instead of viewProviders not fix it?At the host boundary Angular exposes only the boundary component instance and its `viewProviders`. The `providers` array is excluded there, so an item declared in `FaqGroup`'s template still stops without finding `ACCORDION` and throws NG0201.
- Why alias through a separate token rather than viewProviders: [{provide: Accordion, useExisting: Accordion}]?That provider aliases `Accordion` to itself: resolving it from the wrapper's position would first meet the same alias, a circular dependency. A distinct abstract token lets the alias resolve the concrete `Accordion` further up the tree, and also decouples items from one implementation.
saying these in an interview costs you the question
- host stops at the nearest component element in the rendered DOM
- host falls back to root when the boundary lacks the token
- The boundary component's providers are visible to host lookups
- Moving markup into a wrapper cannot change what DI resolves
- Projected content's host boundary is the component it is projected into