In Angular, what is the difference between a pure and an impure pipe, and why does a pure pipe miss an item pushed into an array?
answer
- pure is the default
- compared by reference
- impure runs on each check
- replace the array, don't mutate
basics
~20 sA pure pipe (the default) re-runs only when its value or an argument changes by Object.is, so pushing into the same array is invisible. An impure pipe (pure: false) runs on every check of its view.
solid answer
~40 sPipes are pure by default: Angular keeps the last value and arguments for each binding and calls `transform` only when one of them changes by `Object.is`, meaning by reference for arrays and objects. `users.push(u)` keeps the same reference, so the pure pipe returns its cached output. An impure pipe, `pure: false`, runs `transform` every time its view is checked, so it sees mutations but pays that cost on every check. The preferred fix is an immutable update, such as `users.update(list => [...list, u])`, or deriving the list with `computed()`. And impure is not a full fix in v22: with `OnPush` and zoneless defaults, the view must still be checked for the pipe to run.
code
ts · 26 linesimport { Component, Pipe, PipeTransform, signal } from '@angular/core';
interface User { id: number; name: string; active: boolean; }
@Pipe({ name: 'activeOnly' })
export class ActiveOnlyPipe implements PipeTransform {
transform(users: User[]): User[] {
return users.filter((u) => u.active);
}
}
@Component({
selector: 'app-user-list',
imports: [ActiveOnlyPipe],
template: `
@for (user of users() | activeOnly; track user.id) { <li>{{ user.name }}</li> }
<button (click)="add()">Add</button>
`,
})
export class UserList {
users = signal<User[]>([]);
add() {
// New array reference: the pure pipe runs again
this.users.update((list) => [...list, { id: list.length + 1, name: 'New', active: true }]);
}
}go deeper
Recall that pipes are pure by default and that pure pipes only re-run when the input reference or arguments change.
Explain Object.is comparison on value and arguments, why push() is invisible, and the cost of pure: false.
Prefer immutable updates, signals or computed over impure pipes, and explain why impure still fails when an OnPush view is not checked.
Set data-flow conventions, immutable updates and signals, that make pure pipes and OnPush correct by construction across teams.
## Two kinds of pipe Every Angular pipe is either **pure** or **impure**, set by the `pure` flag in `@Pipe`. Pipes are **pure by default**. - A **pure** pipe's `transform` runs only when the **input value or any argument changes**. Angular compares each one with `Object.is`: primitives by value, objects and arrays by **reference**. - An **impure** pipe (`pure: false`) runs its `transform` **every time the view containing it is checked**, whether or not anything changed. ## The mutated array trap ```ts @Pipe({ name: 'activeOnly' }) export class ActiveOnlyPipe implements PipeTransform { transform(users: User[]): User[] { return users.filter((u) => u.active); } } ``` ```html @for (user of users | activeOnly; track user.id) { <li>{{ user.name }}</li> } ``` If the component does `this.users.push(newUser)`, the list on screen does not change. The array is the **same reference**, so the pure pipe returns its cached result from last time. Angular did not skip the check; it checked the binding, saw an identical input, and reused the output. ## Three fixes, in order of preference 1. **Replace, don't mutate.** Produce a new array: `this.users = [...this.users, newUser]`, or with a signal, `users.update((list) => [...list, newUser])`. The new reference makes the pure pipe run. This keeps the cheap default and matches how signals and `OnPush` expect data to change. 2. **Derive in the component instead.** A `computed()` over a signal holding the list recalculates when the signal changes, and the template reads the result directly. 3. **Mark the pipe impure** with `pure: false`. It will then see mutations, because it runs on every check of its view, but it also re-filters on every check even when nothing changed. The Angular guide warns to avoid impure pipes unless necessary. ## Comparing the two | | Pure (default) | Impure (`pure: false`) | |---|---|---| | When `transform` runs | Value or an argument changed by `Object.is` | Every check of the view | | Sees in-place mutation | No | Yes, when the view is checked | | Cost | Low: at most once per input change | Paid on every check | | Instances | One per usage in a view | One per usage in a view | | Built-in examples | `date`, `currency`, `number` | `async`, `json`, `keyvalue`, `slice` | ## "Every check" has a condition Impure does not mean "runs constantly". It runs when Angular **checks the view that contains it**. In Angular 22, components without a `changeDetection` setting are `OnPush`, and new applications are zoneless, so a view is checked only when something marks it: an input change, a template event, a signal it reads, or an explicit `markForCheck()`. An impure pipe inside an `OnPush` component whose array was mutated silently still shows stale output until something else triggers a check of that component. That is why "make it impure" is rarely a complete fix on its own. ## Why the default is pure A pure pipe is a **per-binding memo** of its last input and output: Angular keeps the most recent arguments for that binding and skips `transform` when they are identical. Most transformations, such as formatting and mapping, depend only on their inputs, so this is both correct and cheap. It is not a global cache: two usages of the same pipe in a template keep separate memos, and only the latest input is remembered. ## How to say it in an interview A crisp answer names the comparison (`Object.is` on value and arguments), the consequence (in-place mutation is invisible to a pure pipe), the preferred fix (immutable updates or signals) and the cost of the alternative (an impure pipe runs on every check of its view).
- If users is a signal and you call users().push(u), does the pure pipe update?No. The signal was not written, so nothing is notified, and the array reference is the same, so the pipe would reuse its cached result even if the view were checked. Write a new array with `set` or `update` instead.
- Which built-in pipes are impure, and why?`async`, `json`, `keyvalue` and `slice` are declared `pure: false`. They must notice changes that keep the same input reference: a new value from a subscription, or mutated contents of an object or array. `keyvalue` limits the cost with a differ that rebuilds only when entries change.
- Is a pure pipe's result cached across different bindings?No. Each usage in a view has its own instance and remembers only its own last input and output. Two bindings with the same input both call transform once each.
A pure pipe is a clerk who only redoes a report when handed a different folder; if you slip a new page into the same folder, the clerk hands back yesterday's copy. An impure pipe redoes the report every time anyone asks, whether or not the folder changed.
saying these in an interview costs you the question
- Thinks a pure pipe deep-compares arrays and objects
- Believes impure pipes run constantly, even when their view is not checked
- Fixes every stale pipe by setting pure: false
- Thinks pipes are impure by default
- Believes a pure pipe caches results globally across all usages