In Angular, how do you inject a service into a custom pipe, which injector resolves it, and how many pipe instances does a template create?
answer
- pipes take part in DI
- resolved like a directive on the element
- no view providers for pipes
- one instance per usage
basics
~10 sCall inject() in the pipe class; it resolves from the element injector where the pipe is used, then environment injectors, without the host's viewProviders. Each usage in each view gets its own pipe instance.
solid answer
~40 sA pipe can call `inject()` in a field initializer or take constructor parameters. Angular resolves them as if the pipe were a directive on the element where it is used: element injectors up the tree, then environment injectors, but explicitly without the host component's `viewProviders`. `@Pipe` has no `providers` option. Angular creates one pipe instance per usage in a view, so two bindings in one template are two instances and a pipe in an `@for` row has one per row; instances die with their view and can implement `ngOnDestroy`. Shared state belongs in an injected service, and the reverse direction, injecting a pipe class into a service, is discouraged: extract a plain function instead.
code
ts · 18 linesimport { Injectable, Pipe, PipeTransform, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class StatusLabels {
private readonly labels: Record<string, string> = { A: 'Active', S: 'Suspended' };
lookup(code: string): string {
return this.labels[code] ?? code;
}
}
@Pipe({ name: 'statusLabel' })
export class StatusLabelPipe implements PipeTransform {
private readonly labels = inject(StatusLabels);
transform(code: string): string {
return this.labels.lookup(code);
}
}go deeper
Recall that a pipe can use inject() like any other Angular class, and that the service should hold shared data.
Explain that pipes resolve from the element where they are used, without that element's component viewProviders, and that each usage creates an instance.
Keep pipes stateless, pass changing data as arguments so purity holds, and extract logic into functions rather than injecting pipes.
Define where cross-cutting formatting state lives, services, tokens or signals, so pipes stay thin and predictable across teams.
## Injecting into a pipe A pipe class takes part in dependency injection just like a directive. The current style is the **`inject()` function** in a field initializer: ```ts @Pipe({ name: 'statusLabel' }) export class StatusLabelPipe implements PipeTransform { private readonly labels = inject(StatusLabels); transform(code: string): string { return this.labels.lookup(code); } } ``` Constructor parameters work too, but `inject()` is the recommendation for new code. ## Which injector resolves it A pipe is resolved from the **element injector of the node where the pipe is used**, then up the element-injector tree, then the environment injectors, as if the pipe were a directive on that element. Two consequences: - providers declared by a component or directive on an **ancestor element** are visible to the pipe; - a component's **`viewProviders` are not** visible to pipes used on that component's host element; the framework explicitly disables that access when it creates the pipe. Pipes themselves cannot declare `providers`; `@Pipe` has no such option. ## How many instances exist Angular creates **one pipe instance per usage in a view**: - `{{ a | statusLabel }}` and `{{ b | statusLabel }}` in the same template are two instances; - a pipe inside an `@for` row gets a new instance for each row's view; - the instances are destroyed with their view, and a pipe that implements `ngOnDestroy` gets that hook called. So a pipe instance is not a singleton, and state stored on it is per usage. Anything that must be shared, such as a cache of translations, belongs in an injected service. | Question | Answer | |---|---| | Can a pipe call `inject()`? | Yes, in a field initializer or the constructor | | Starting point for resolution | The element where the pipe is used | | Sees the host component's `viewProviders`? | No | | Instances per template | One per usage, per view | | Lifecycle hook available | `ngOnDestroy` | ## What to inject, and what not to - **Good fits**: stateless lookups, formatting configuration such as `LOCALE_ID`, a shared cache. - **Careful**: a service whose data changes over time. A pure pipe will not re-run just because the service's data changed; pass the changing value as an argument, or read it from a signal passed in, so Angular can see the dependency. - **Avoid**: services that perform HTTP calls from `transform`. The pipe is invoked during change detection; a request per evaluation is easy to trigger by accident, and the result arrives asynchronously anyway, which is the async pipe's job. ## The reverse direction: pipes in services The Angular guide advises **against injecting a pipe class into a service or component** to reuse its logic. Pipes are template operators, not injectable services. Extract the transformation into a plain exported function, have the pipe's `transform` delegate to it, and import the function wherever else it is needed. For built-in pipes, the equivalent functions are exported from `@angular/common`: `formatDate`, `formatCurrency`, `formatNumber` and `formatPercent`. ## A worked diagnosis A team reports that a `currencyLabel` pipe keeps returning a stale label after the user switches the display currency in settings. The pipe injects a `Settings` service and reads `settings.currency` inside `transform`. The pipe is pure, and its only input, the amount, did not change, so Angular never called `transform` again. The fix is not `pure: false`; it is to make the dependency visible: `{{ amount | currencyLabel: settings.currency() }}` with the setting held in a signal, so both the view refresh and the pipe re-run happen when it changes. ## Testing a pipe with dependencies 1. For a pipe without dependencies, instantiate it directly and call `transform`. 2. For a pipe that calls `inject()`, create it inside an injection context, for example with `TestBed.runInInjectionContext(() => new StatusLabelPipe())`, after providing test doubles through `TestBed.configureTestingModule`. 3. Test the extracted plain function separately; most of the logic lives there.
- A pure pipe injects a settings service whose value changes at runtime; why does the output not update?A pure pipe re-runs only when its value or arguments change. A change inside an injected service is invisible to that comparison. Pass the changing setting as an argument, ideally read from a signal in the template, so Angular sees the dependency.
- How do you unit-test a pipe that calls inject()?Configure providers with `TestBed.configureTestingModule`, then create the pipe inside an injection context, for example `TestBed.runInInjectionContext(() => new StatusLabelPipe())`, and call `transform` directly.
saying these in an interview costs you the question
- Believes a pipe is a singleton shared by the whole app
- Adds a providers array to the @Pipe decorator
- Expects a pipe to see the host component's viewProviders
- Makes HTTP calls from a pipe's transform method
- Injects a pipe class into a service to reuse its logic