Custom Pipes and Pure Change Detection
Greenhouse keeps writing "Water every 1 days". You want "1 day" and "3 days", everywhere, without an @if in every template. When no built-in pipe does what you need, you write one. It takes a few lines, plus one idea about when Angular runs it.
The rule
A pipe is a pure function in a class; same input, no rerun.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'countLabel' })
export class CountLabelPipe implements PipeTransform {
transform(count: number, word: string): string {
return `${count} ${word}${count === 1 ? '' : 's'}`;
}
}@Pipe({ name })gives the name used in templates, in camelCase.transformreceives the value on the left of|first, then each argument after a colon.- Like components, pipes are standalone: import the class where you use it.
<p>Water every {{ plant().everyDays | countLabel:'day' }}</p>Pure by default. Angular treats a pipe as a pure function: the same inputs always give the same output. So it caches the result and re-runs transform only when an input changes, meaning a new primitive value or a new object reference. That makes pipes cheap, even in long lists.
The catch comes with objects and arrays. Suppose a pure thirstyOnly pipe filters the plant array:
this.plants().push(newPlant); // same array: pipe does NOT re-run
this.plants.update(list => [...list, newPlant]); // new array: pipe re-runsMutating in place keeps the same reference, so Angular sees "same input" and shows the old result. Replace instead of mutate, and everything updates.
You can mark a pipe pure: false to run it on every change detection pass, but it then runs very often. For filtering and sorting, a computed signal in the class is usually the better tool:
thirsty = computed(() => this.plants().filter(p => p.daysSince > p.everyDays));The shortcut
Write a pipe in three moves: name it, write transform(value, ...args), import it. Then ask "will my input ever change without becoming a new object?" If yes, switch to immutable updates or a computed signal rather than an impure pipe.
Name it
@Pipe({ name: 'countLabel' })so templates write| countLabel.
Drills
Answer fast — trust the rule.
In {{ 4 | countLabel:'pot' }}, what does transform receive?
By default, when does Angular re-run a pure pipe?
A pure filter pipe does not update after this.plants().push(p). Why?
What is usually the best fix for that filtered list?
Remember
- A custom pipe:
@Pipe({ name }), atransform(value, ...args)method, and an import where used. - Pure by default: it re-runs only for a new value or reference.
- Trap: mutating arrays in place; replace them, or filter with
computedinstead.
