Skip to main content

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:

  1. It returns the same output when given the same input arguments
  2. It doesn't maintain internal state
  3. 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:

bash
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):

typescript
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 (|):

html
<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:

typescript
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:

html
<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:

  1. The input value changes (through object reference)
  2. 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:

typescript
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();
}
}
typescript
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:

typescript
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:

html
<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:

typescript
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:

html
<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:

  1. Observable Data: For async data streams, use the built-in async pipe instead
  2. 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

  1. Use Pure Pipes for Transformations: Whenever you need to transform data for display, consider using a pure pipe
  2. Make Pipes Reusable: Design pipes to be generic enough to use across your application
  3. Keep Transformations Simple: Each pipe should have a single responsibility
  4. Be Mindful of Reference Changes: Remember that pure pipes depend on reference changes for detecting changes to objects and arrays
  5. 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:

typescript
@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

Exercises

  1. Create a pure pipe that formats phone numbers from 1234567890 to (123) 456-7890
  2. Build a pipe that truncates text to a specified length and adds an ellipsis
  3. Implement a pure pipe that converts an array of strings into a comma-separated string with an optional limit parameter
  4. Create a pipe that formats dates relative to current time (e.g., "2 days ago", "5 minutes ago")
  5. 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! :)