In an Angular template, how does {{ formatPrice(item) }} differ from {{ item.price | price }} in when your code actually runs?
answer
- bindings are re-evaluated on checks
- a method's dependencies are unknown
- per-binding memo on arguments
- dev-mode verification pass
basics
~20 sA method call in a binding runs every time the view is checked, because Angular cannot know its dependencies. A pure pipe runs only when its value or arguments change, reusing its previous result otherwise.
solid answer
~30 sAngular re-evaluates every binding expression each time it checks the view, so `formatPrice(item)` runs on every check, once per row in an `@for`, and again in the development-mode verification pass after each tick. It cannot know the method depends only on `item`. A pure pipe declares its dependencies: Angular keeps the last value and arguments for that binding and calls `transform` only when one changes by `Object.is`. That makes the pipe cheaper and reusable across templates, at the price of being blind to in-place mutation. For state inside one component, a `computed()` signal is often clearer than either.
code
ts · 23 linesimport { Component, Pipe, PipeTransform, input } from '@angular/core';
@Pipe({ name: 'price' })
export class PricePipe implements PipeTransform {
transform(cents: number, currency = 'EUR'): string {
return `${(cents / 100).toFixed(2)} ${currency}`;
}
}
@Component({
selector: 'app-price-row',
imports: [PricePipe],
template: `
<td>{{ formatPrice(item().priceCents) }}</td> <!-- runs on every check -->
<td>{{ item().priceCents | price }}</td> <!-- runs when priceCents changes -->
`,
})
export class PriceRow {
item = input.required<{ priceCents: number }>();
formatPrice(cents: number) {
return `${(cents / 100).toFixed(2)} EUR`;
}
}go deeper
Recall that a method in a binding runs whenever the view is checked, while a pure pipe runs only when its inputs change.
Explain why Angular cannot memoise methods, how pure pipes compare inputs, and why dev builds evaluate bindings twice per tick.
Choose between a pure pipe, a computed() signal and a method based on reuse, dependencies and mutation patterns, not on habit.
Set template guidance for a codebase, such as no computation in bindings, and back it with profiling rather than folklore.
## The two templates ```html <!-- A: method call --> <td>{{ formatPrice(item) }}</td> <!-- B: pure pipe --> <td>{{ item.price | price: currency }}</td> ``` Both render the same text. They differ in **when Angular calls your code**. ## A: a method call runs on every check A template binding is an expression Angular evaluates **each time it checks the view**. A method call is just part of that expression, so `formatPrice(item)` runs: - on every check of that component's view, whether or not `item` changed; - once per row when it sits inside an `@for`; - again during the **development-mode verification pass**: after each application tick, development builds re-evaluate bindings to detect values that changed after they were checked, so a `console.log` inside the method appears more often than you expect. Angular cannot know that `formatPrice` depends only on `item`: a method could read anything on `this`, so it must be called to find out. ## B: a pure pipe runs when its inputs change For a pure pipe Angular stores the last value and arguments **for that binding** and compares the new ones with `Object.is`. If they are identical, it returns the previous result without calling `transform`. In a list of 500 rows where one price changes, only that row's pipe runs again. ## Side by side | | Method call in a binding | Pure pipe | |---|---|---| | Runs when | Every check of the view | Value or an argument changed | | Knows its dependencies | No, it may read anything | Yes: exactly its value and arguments | | Reusable across components | Only by copying or sharing the method | Import the pipe anywhere | | Can read component state freely | Yes | Only what is passed as arguments | | Result on in-place mutation | Always fresh | Stale until the reference changes | That last row is the trade-off: a method is always fresh because it always runs, while a pure pipe is cheap because it trusts references. ## Where signals fit In current Angular a third option is often the best: a **`computed()` signal** in the component. It recalculates only when a signal it reads changes, and the template reads a value rather than calling logic. For a value derived from component state, `computed()` is usually clearer than both. A pipe remains the better tool for a **reusable, stateless formatter** used in many templates, where the input comes from the binding itself, such as per-row values inside an `@for`. Reading a signal in a template, as in `{{ total() }}`, is a function call too, but a cheap one: it returns the current value, and a `computed` recalculates only when its sources changed. Interviewers do not mean signal reads when they warn against function calls in templates. ## When a method call is fine - The method is trivial and cheap, and the component is checked rarely. - The value genuinely depends on state that no single argument captures, and you have measured no problem. - It is an event handler, as in `(click)="save()"`; the rule is about bindings evaluated during checks, not about events. ## A quick way to see the difference Add a `console.count('formatPrice')` inside the method and a `console.count('price pipe')` inside `transform`, then click an unrelated button in the component a few times. The method's counter climbs on every click, because each click checks the view; the pipe's counter stays still until a price actually changes. This small experiment is often what convinces a team, and it is a good answer when an interviewer asks how you would prove the claim rather than just state it. ## The interview answer in four points 1. A binding's method runs every time the view is checked, including development-mode verification passes. 2. A pure pipe is memoised per binding on its value and arguments, compared by `Object.is`. 3. That makes pipes cheaper and reusable, but blind to in-place mutation. 4. For state inside one component, a `computed()` signal is often the cleanest choice.
- Why does a console.log in a template method fire more than once per change in a development build?Every check re-evaluates the binding, and in development builds Angular runs an extra verification pass after each tick that evaluates bindings again to detect values that changed after being checked. A pure pipe with unchanged inputs is skipped in both passes.
- Is {{ total() }} on a signal the same problem as calling a method?No. A signal read returns a stored value and registers a dependency, so it is cheap and lets Angular know when to refresh. The warning is about methods that compute something on every check.
saying these in an interview costs you the question
- Believes Angular caches method results in templates automatically
- Thinks a pure pipe re-runs whenever its view is checked
- Treats signal reads in templates as expensive method calls
- Thinks event-handler method calls have the same cost issue
- Assumes a pure pipe sees mutations inside its input object