skip to content

In Angular, how do the self, skipSelf and host options of inject() change where a dependency lookup starts and stops?

level: middleimportance: must knowfreq 58%

answer

  1. one moves the start, two move the end
  2. current element only
  3. begin at the parent
  4. boundary of the declaring template
  5. no environment fallback for two of them

basics

~20 s

skipSelf starts the search at the parent instead of the current element; self checks only the current element's injector; host stops at the host of the template the requester is declared in. self and host never fall back to environment injectors.

solid answer

~40 s

`skipSelf` moves the **start**: the requesting element's own injector is skipped, and the walk continues through ancestors and then environment injectors as usual. `self` and `host` move the **end**. `self` checks only the current element's injector and never consults environment injectors — `inject(HttpClient, {self: true})` in a component throws NG0201. `host` lets the walk climb element injectors only up to the host element of the component whose template declares the requester; at that boundary only that component and its `viewProviders` are visible, not its `providers`, and environment injectors are not consulted. `self` cannot be combined with `skipSelf` or `host`; the others combine, typically with `optional`. Constructor injection uses `@Self()`, `@SkipSelf()` and `@Host()`.

code

ts · 15 lines
ts
import {Directive, inject} from '@angular/core';
import {NgControl} from '@angular/forms';

@Directive({selector: 'input[appTrimOnBlur]', host: {'(blur)': 'trim()'}})
export class TrimOnBlur {
  // Only the NgControl on this same <input>; null if the input has no formControl/ngModel.
  private control = inject(NgControl, {self: true, optional: true});

  trim() {
    const value = this.control?.value;
    if (typeof value === 'string') {
      this.control?.control?.setValue(value.trim());
    }
  }
}

go deeper

for a junior

Remember that skipSelf starts at the parent, self checks only the current element, and host stops at the component boundary.

for a middle

Explain which options disable the environment-injector fallback, why self plus a root service throws NG0201, and which combinations are illegal.

for a senior

Reason about host precisely: the declaring template's boundary, projected content, and why only viewProviders are visible at the boundary, as the forms ControlContainer pattern shows.

for a principal

Decide when a library should rely on narrowed lookups for encapsulation versus explicit inputs, since host and self rules are subtle for consumers to reason about.

## The default walk the options modify Without options, a lookup from a component or directive: 1. checks the requesting element's injector; 2. climbs ancestor element injectors; 3. then walks environment injectors — route, `root`, platform — up to the `NullInjector`, which throws NG0201. The Angular guide groups the options by what they change: **`optional`** changes what happens on a miss, **`skipSelf`** changes where the search **starts**, and **`self`** and **`host`** change where it **stops**. | Option | Starts at | Stops at | Environment injectors searched? | Constructor form | |---|---|---|---|---| | (none) | current element | `NullInjector` | yes | — | | `skipSelf` | parent element | `NullInjector` | yes | `@SkipSelf()` | | `self` | current element | current element | **no** | `@Self()` | | `host` | current element | host of the declaring template | **no** | `@Host()` | ## `skipSelf`: begin at the parent `skipSelf` ignores providers on the requesting element and starts with its parent. The rest of the walk is unchanged, including environment injectors. Typical uses: - a component that **provides** a service and also needs the ancestor's instance of the same token, for example to delegate to it; - a component looking for an **ancestor of its own type** — a nested accordion finding its parent accordion. Without `skipSelf` it would find itself and fail with NG0200 (circular dependency). The outermost instance has no ancestor, so this is paired with `optional`. ## `self`: this element only `self` checks only the requesting element's injector — the `providers` of components and directives on that element, including the other directives themselves — and then stops. It does **not** fall back to environment injectors, so a root-provided service injected with `self: true` from a component throws NG0201. Uses: - a directive that wants a collaborator **on the same element only**, such as the `NgControl` from a `formControlName` or `ngModel` on the same input: `inject(NgControl, {self: true, optional: true})`; - a component that must use its own instance and never an ancestor's. ## `host`: stop at the template boundary `host` is the least intuitive option. The walk climbs element injectors from the requester, but only within the **template in which the requester is declared**. The last stop is the host element of the component that owns that template, and at that boundary Angular exposes **only that component instance and its `viewProviders`** — not its `providers`, and not other directives on the host element. It never falls through to environment injectors. The option documentation also notes it is a no-op for environment injectors. Consequences worth knowing: - Content **projected** into a component is declared in the outer template, so its `host` boundary is the outer component, and the component it is projected into is just an intermediate element with all its `providers` visible. - A component's own `viewProviders` are reachable with `host` from its template; its `providers` are not. Angular's forms package relies on this: a child component re-exposes the parent form with `viewProviders: [{provide: ControlContainer, useExisting: NgForm}]`, and its inner controls — which look up their `ControlContainer` with host-limited lookups — find it at the boundary. ## Combinations - `self` cannot be combined with `skipSelf` (contradictory start) or with `host` (both define the end). - `skipSelf` + `host`: begin at the parent, stop at the boundary — the forms directives' pattern for finding their parent group. - Any of them + `optional`: narrowed lookups miss more often, and `optional` turns that miss into `null`. ## A worked example Take `<app-shell>` with `providers: [Theme]` rendering `<app-card>` with its own `providers: [Theme]`, and a `ThemeBadge` directive on `<app-card>`'s element, while `root` also provides a `Theme`: 1. `inject(Theme)` in `ThemeBadge` — found on its own element: the card's `Theme`. 2. `inject(Theme, {skipSelf: true})` — starts at `<app-shell>`: the shell's `Theme`. 3. `inject(Theme, {self: true})` — the card element's `Theme`; had the card not provided one, NG0201, never root's. 4. `inject(Theme, {host: true})` from the directive, which is declared in the shell's template — the card element is intermediate, so the card's `Theme` is found; without it, the walk would stop at `<app-shell>`, where only the shell's `viewProviders` count, so the shell's `providers` entry would be missed. ## Decorators versus options The constructor decorators `@Self()`, `@SkipSelf()`, `@Host()` and `@Optional()` express the same flags on parameters; `inject()` takes them as an options object. The old numeric `InjectFlags` enum was removed in Angular 20.

  • Why does inject(HttpClient, {self: true}) in a component throw even though provideHttpClient() is in ApplicationConfig?
    `self` limits the lookup to the requesting element's own injector and disables the fallback to environment injectors. `HttpClient` lives in the root environment injector, so it is never consulted and the lookup throws NG0201. Drop `self` for environment-level services; it only makes sense for element-level collaborators.
  • With host: true, why can a child in a component's template see that component's viewProviders but not its providers?
    At the host boundary Angular only exposes the component instance and its `viewProviders`, which by definition belong to the component's own view. `providers` are the host element's public, content-facing providers and are excluded at that boundary. This is why forms re-expose a parent form with `viewProviders` and `useExisting`.
  • Can self and skipSelf be combined?
    No. `self` means check only the current element, `skipSelf` means do not check the current element, so together they would search nothing. The Angular guide lists self+skipSelf and self+host as the two combinations you cannot use; `skipSelf`+`host` and any option with `optional` are fine.

A courier looking for a signature: skipSelf means start with the neighbour's door; self means only this door; host means knock only on doors inside this building, including the building manager's office, but never go out to city hall.

saying these in an interview costs you the question

  • self still falls back to root if the element lacks a provider
  • skipSelf skips all element injectors and goes straight to root
  • host stops at the nearest parent component in the DOM
  • host sees the boundary component's providers array
  • self and skipSelf can be combined to check only the parent