Angular pipes transform values in templates using the | operator (formatting, slicing, mapping) without mutating the source. Most pipes are pure (run only when input reference/primitive changes). The async pipe is special: it subscribes/unsubscribes automatically and triggers view updates.
Angular pipes: built-in, async pipe, and pure vs impure (performance + pitfalls)
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
A pipe is a reusable template-level transformer: it takes an input value and returns a derived value for display. It should be side-effect free and should not mutate the input.
Concept | What it means in practice |
|---|---|
Syntax |
|
Chaining |
|
Where it runs | In Angular’s change detection update phase (template evaluation). |
What it’s for | Formatting + small/cheap view transforms; not heavy computation or side effects. |
<p>{{ today | date:'fullDate' }}</p>
<p>{{ username | uppercase }}</p>
<p>{{ price | currency:'EUR':'symbol':'1.2-2' }}</p>
<p>{{ completion | percent:'1.0-0' }}</p>
<pre>{{ user | json }}</pre>
Built-in pipe | Typical use |
|---|---|
| Date/time formatting (uses locale + optional timezone) |
| Money formatting (currency code, display, digitsInfo) |
| Numeric formatting with digitsInfo |
| Percent formatting |
| Debug display of objects (not for production UI) |
| Substrings / subarrays in templates (keep usage cheap) |
| Iterate object maps in templates |
| Subscribe to Observable/Promise and render latest value safely |
The async pipe is the one that matters most in real appsasync subscribes/unsubscribes automatically, avoids manual subscription leaks, and marks the view for update when a new value arrives (works great with OnPush).
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { Observable, timer, map } from 'rxjs';
@Component({
selector: 'app-demo',
standalone: true,
imports: [NgIf, AsyncPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p *ngIf="count$ | async as count">Count: {{ count }}</p>
`
})
export class DemoComponent {
readonly count$: Observable<number> = timer(0, 1000).pipe(map(t => t));
}
Pure vs impure pipes (performance hotspot)
Type | When it runs | Pros | Cons / risk |
|---|---|---|---|
Pure (default) | Only when input changes by value (primitives) or by reference (objects/arrays/functions). | Fast; avoids re-running on every CD pass. | If you mutate arrays/objects in place, the pipe may not re-run (because reference didn’t change). |
Impure ( | Every change detection cycle. | Can react to in-place mutation or time-varying global state. | Can become very expensive and cause jank; use sparingly. |
Classic pure-pipe gotcha
If you do items.push(...), the array reference stays the same, so a pure pipe may not run again. Prefer immutable updates (items = [...items, newItem]) or move the logic out of the template.
Custom pipes: keep them pure + small
Custom pipes are great for reusable formatting/transforms. Keep them deterministic, no side effects, no DOM, no HTTP. In modern Angular you can make them standalone and import them directly.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'titleCaseWords',
standalone: true
})
export class TitleCaseWordsPipe implements PipeTransform {
transform(value: string | null | undefined): string {
if (!value) return '';
return value
.trim()
.split(/\s+/)
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(' ');
}
}
import { Component } from '@angular/core';
import { TitleCaseWordsPipe } from './title-case-words.pipe';
@Component({
selector: 'app-user',
standalone: true,
imports: [TitleCaseWordsPipe],
template: `{{ name | titleCaseWords }}`
})
export class UserComponent {
name = 'ada lovelace';
}
Pitfall | What goes wrong | Better approach |
|---|---|---|
Heavy work inside a pipe used in big lists | Even pure pipes can run often enough to hurt if the transform is expensive. | Precompute in TS, memoize by key, or transform upstream in RxJS/store selectors. |
Using an impure pipe to “fix” mutation | Runs every CD cycle; can tank performance. | Prefer immutable state updates or move logic out of template. |
Doing side effects in pipes | Unpredictable behavior; multiple calls; hard to test. | Keep pipes pure; side effects belong in services/effects/components. |
Manual subscribe in component just to render | Leaks/unsubscribe complexity; extra state variables. | Use |
Calling methods in templates instead of pipes | Method is executed every CD pass; can be worse than a pure pipe. | Use pure pipes for cheap transforms, or cache results explicitly. |
Pipes are template transformers used via |. Default is pure: runs when inputs change by value/reference (fast, but mutation-in-place can fool it). Impure pipes run every CD cycle (use rarely). The async pipe is key: it manages subscriptions and keeps templates clean—especially with OnPush.