Angular Pure Pipes
Introduction
Pipes in Angular are a powerful feature that allow you to transform data directly in your templates before displaying it to the user. When building high-performance Angular applications, understanding the distinction between pure and impure pipes is crucial. In this guide, we'll focus specifically on pure pipes, which are Angular's recommended approach for data transformations due to their performance benefits.
Pure pipes follow the concept of pure functions from functional programming—given the same input, they always return the same output without side effects. This predictability allows Angular to optimize performance by caching the results and only recalculating when necessary.
What Makes a Pipe "Pure"?
A pipe is considered pure when:
- It returns the same output when given the same input arguments
- It doesn't maintain internal state
- It doesn't produce side effects (like HTTP requests or DOM manipulation)
By default, all Angular pipes are pure. This is intentional as pure pipes align with Angular's change detection strategy and help maintain performance.
Creating Your First Pure Pipe
Let's create a simple pure pipe that capitalizes the first letter of each word in a string.
First, we generate a pipe using the Angular CLI:
ng generate pipe capitalize-words
This creates a file named capitalize-words.pipe.ts
. By default, the pipe is pure (note the pure: true
property):
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'capitalizeWords',
pure: true // This is actually the default, so you can omit it
})
export class CapitalizeWordsPipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
}
Using Pure Pipes in Templates
Once you've created a pipe and included it in your module's declarations, you can use it in templates with the pipe operator (|
):
<div>
<p>Original: {{ userInput }}</p>
<p>Transformed: {{ userInput | capitalizeWords }}</p>
</div>
Input and Output:
- If
userInput = "hello angular developers"
- Output will display:
"Hello Angular Developers"
Pure Pipes with Parameters
Pipes can accept parameters to customize their behavior. Let's enhance our pipe to accept a parameter that determines whether to capitalize all letters or just the first letter of each word:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'capitalizeWords',
pure: true
})
export class CapitalizeWordsPipe implements PipeTransform {
transform(value: string, allCaps: boolean = false): string {
if (!value) return '';
return value
.split(' ')
.map(word => {
if (allCaps) {
return word.toUpperCase();
}
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(' ');
}
}
In your template, you can pass parameters after the pipe name:
<p>Standard Capitalization: {{ userInput | capitalizeWords }}</p>
<p>All Caps: {{ userInput | capitalizeWords:true }}</p>
Performance Benefits of Pure Pipes
The key advantage of pure pipes is their performance. Angular only recalculates the pipe's output when:
- The input value changes (through object reference)
- Any pipe parameters change
For primitive types (strings, numbers, booleans), Angular checks for value changes. For objects and arrays, it checks for reference changes, not deep equality.
Example Demonstrating Caching Behavior
Let's see how Angular caches pure pipe results:
import { Component } from '@angular/core';
@Component({
selector: 'app-pure-pipe-demo',
template: `
<p>Current time using function: {{ getCurrentTime() }}</p>
<p>Current time using pure pipe: {{ 'time' | timeStamp }}</p>
<button (click)="triggerChange()">Trigger Change Detection</button>
`
})
export class PurePipeDemoComponent {
triggerChange() {
// Just to trigger change detection
console.log('Change detection triggered');
}
getCurrentTime() {
return new Date().toLocaleTimeString();
}
}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'timeStamp',
pure: true
})
export class TimeStampPipe implements PipeTransform {
transform(value: any): string {
console.log('TimeStamp pipe executed');
return new Date().toLocaleTimeString();
}
}
In this example:
- The function-based approach will update the time whenever change detection runs
- The pure pipe will only run once and cache the result, not updating on subsequent change detection cycles
This demonstrates how pure pipes can significantly reduce unnecessary calculations in your templates.
Real-World Use Cases for Pure Pipes
1. Formatting Data
Pure pipes are excellent for formatting data because they implement transformations that don't require state:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fileSize',
pure: true
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
}
Usage:
<p>Document size: {{ 1500000 | fileSize }}</p>
<!-- Outputs: "Document size: 1.43 MB" -->
<p>Image size: {{ 2048 | fileSize:0 }}</p>
<!-- Outputs: "Image size: 2 KB" -->
2. Filtering and Sorting Collections
Pure pipes are great for filtering and sorting data in templates:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filter',
pure: true
})
export class FilterPipe implements PipeTransform {
transform(items: any[], searchText: string, field: string): any[] {
if (!items) return [];
if (!searchText) return items;
searchText = searchText.toLowerCase();
return items.filter(item => {
return item[field].toLowerCase().includes(searchText);
});
}
}
Usage:
<input type="text" [(ngModel)]="searchTerm" placeholder="Search products...">
<ul>
<li *ngFor="let product of products | filter:searchTerm:'name'">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
Important Note: While filtering using pure pipes works, for large data sets, it's better to handle filtering in the component class instead, as pure pipes will re-run the entire filter operation even for small changes in the search term.
When to Avoid Pure Pipes
There are scenarios where pure pipes are not ideal:
- Observable Data: For async data streams, use the built-in
async
pipe instead - Frequent Updates to Objects/Arrays: If you're continuously making internal changes to objects/arrays without changing their references, pure pipes won't detect these changes
Best Practices
- Use Pure Pipes for Transformations: Whenever you need to transform data for display, consider using a pure pipe
- Make Pipes Reusable: Design pipes to be generic enough to use across your application
- Keep Transformations Simple: Each pipe should have a single responsibility
- Be Mindful of Reference Changes: Remember that pure pipes depend on reference changes for detecting changes to objects and arrays
- Immutability: When working with objects and arrays in Angular, prefer immutable data patterns by creating new references rather than modifying existing ones
Common Pure Pipe Gotchas
Objects and Arrays Need New References
One common issue with pure pipes is that they don't detect internal changes to objects or arrays:
@Component({
selector: 'app-todo-list',
template: `
<ul>
<li *ngFor="let task of tasks | completedFilter">{{ task.name }}</li>
</ul>
<button (click)="markFirstAsCompleted()">Complete First Task</button>
`
})
export class TodoListComponent {
tasks = [
{ name: 'Learn Angular', completed: false },
{ name: 'Master Pipes', completed: false },
{ name: 'Build App', completed: false }
];
markFirstAsCompleted() {
// This won't trigger the pipe to re-run
this.tasks[0].completed = true;
// Instead, create a new array reference
// this.tasks = [...this.tasks];
}
}
In this example, mutating a property of an object in the array doesn't change the array reference, so the pipe won't re-run. The solution is to create a new array reference as shown in the commented code.
Summary
Angular pure pipes are a powerful mechanism for transforming data directly in templates with minimal performance impact:
- They follow functional programming principles by being predictable and side-effect free
- Angular optimizes their execution by caching results and only recalculating when inputs change
- They're excellent for formatting, filtering, and other data transformations
- They work best with immutable data patterns
By understanding and utilizing pure pipes effectively, you can write more declarative, cleaner template code while maintaining high performance in your Angular applications.
Additional Resources
- Official Angular Pipe Documentation
- Angular API: PipeTransform
- Understanding Change Detection in Angular
Exercises
- Create a pure pipe that formats phone numbers from
1234567890
to(123) 456-7890
- Build a pipe that truncates text to a specified length and adds an ellipsis
- Implement a pure pipe that converts an array of strings into a comma-separated string with an optional limit parameter
- Create a pipe that formats dates relative to current time (e.g., "2 days ago", "5 minutes ago")
- Try converting one of your pipes to an impure pipe and observe the performance differences
By mastering pure pipes, you're taking an important step toward building more performant Angular applications!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)