Introduction
Pipes in Angular are one of the most elegant and useful features of the framework. They allow you to transform data directly within templates, providing a clean and declarative way to present data without cluttering the component logic.
Whether it’s formatting dates, converting numbers, capitalizing text, or even transforming asynchronous data streams, pipes offer a consistent and readable solution. However, like all powerful tools, they must be used carefully to maintain performance and clarity.
This guide explores the best practices for using, creating, and managing Angular pipes, helping you write cleaner, faster, and more maintainable code.
1. Understanding Pipes in Angular
Pipes are functions that take an input value, transform it, and return a new value.
In templates, they’re applied using the pipe operator (|
).
Example:
<p>{{ user.name | uppercase }}</p>
This example converts the user.name
string into uppercase before rendering.
Angular provides several built-in pipes, such as:
DatePipe
— Formats date values.CurrencyPipe
— Displays values as currency.DecimalPipe
— Formats decimal numbers.PercentPipe
— Converts numbers to percentages.AsyncPipe
— Subscribes to Observables or Promises automatically.
You can also create custom pipes for specialized transformations.
2. Keep Pipes Pure Unless Necessary
What Are Pure Pipes?
A pure pipe is executed only when the input value changes. It is the default behavior in Angular and is the preferred approach for most use cases.
Pure pipes are efficient because Angular caches the result and reuses it until the input reference changes.
Example:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'reverseString'
})
export class ReverseStringPipe implements PipeTransform {
transform(value: string): string {
return value.split('').reverse().join('');
}
}
This pipe will only re-run when the input string changes, minimizing unnecessary computations.
What Are Impure Pipes?
An impure pipe runs on every change detection cycle, even if the input hasn’t changed.
@Pipe({
name: 'impurePipe',
pure: false
})
Use impure pipes only when absolutely necessary, such as when transforming collections that change frequently (e.g., filtering arrays dynamically).
However, impure pipes can degrade performance because they re-run very often.
Best Practice:
✔ Always use pure pipes unless you have a strong reason to make them impure.
3. Avoid Putting Heavy Logic Inside Pipes
Pipes are meant for lightweight transformations, not for performing complex or CPU-intensive computations.
For example, avoid using pipes to:
- Make HTTP requests
- Perform nested loops or sorting large datasets
- Trigger side effects such as logging or writing to local storage
Example of what not to do:
transform(value: string): string {
// Bad practice: Calling an API inside a pipe
this.http.get('/api/data').subscribe();
return value;
}
Pipes should be pure functions — given the same input, they must always return the same output without side effects.
If you need to perform heavy data manipulation, do it in:
- The component class using RxJS operators, or
- A service designed for data processing.
Best Practice:
✔ Keep pipe logic simple and stateless. Use pipes only for display-related transformations.
4. Use AsyncPipe Instead of Manual Subscriptions
When working with Observables, you might be tempted to subscribe manually inside your component:
this.dataService.getData().subscribe(data => {
this.items = data;
});
However, this approach requires manual cleanup to prevent memory leaks.
Instead, you can use the AsyncPipe
directly in your template:
<ul>
<li *ngFor="let item of items$ | async">{{ item.name }}</li>
</ul>
Why AsyncPipe Is Better
- Automatically subscribes and unsubscribes.
- Simplifies your component code.
- Prevents memory leaks.
- Makes the template declarative and reactive.
Example with RxJS:
items$ = this.dataService.getData();
The component now only needs to expose the Observable; Angular handles the rest.
Best Practice:
✔ Always use AsyncPipe
for rendering Observables or Promises in templates.
✔ Avoid manual subscriptions unless absolutely necessary for side effects.
5. Create Reusable Custom Pipes for Consistent Transformations
When you find yourself reusing the same transformation logic in multiple components, that’s a clear sign to create a custom pipe.
Example: Creating a CapitalizePipe
to capitalize the first letter of a string.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'capitalize' })
export class CapitalizePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
}
}
Usage:
<p>{{ 'angular' | capitalize }}</p>
Output:
Angular
By creating custom pipes, you:
- Centralize your formatting logic.
- Make your templates cleaner.
- Ensure consistent data presentation across the app.
Best Practice:
✔ Create custom pipes for repeated display logic.
✔ Store them in a shared module so they can be reused across the application.
6. Use Pipes for Display Logic Only
Pipes are part of Angular’s presentation layer, meaning they should only affect how data is displayed, not how it is stored, fetched, or processed.
For instance:
- Formatting dates → good use.
- Converting currencies → good use.
- Sorting data from an API → better handled in the component or service.
Example (Good Use):
<p>{{ user.balance | currency:'USD' }}</p>
Example (Bad Use):
<p>{{ userList | sort:'name' }}</p>
Sorting or filtering large datasets in a pipe can slow down rendering, especially when the pipe runs repeatedly.
Best Practice:
✔ Use pipes for presentation, not business logic.
7. Combine Pipes Wisely
Angular allows pipe chaining, which can simplify templates but should be used judiciously.
Example:
<p>{{ user.name | lowercase | slice:0:10 }}</p>
This first converts the name to lowercase, then slices the first ten characters.
While chaining makes templates expressive, overusing it can reduce readability. If you find yourself chaining three or more pipes, consider handling part of the transformation in your component.
Best Practice:
✔ Chain pipes for simple, readable transformations.
✔ Move complex logic to the component class.
8. Organize and Export Pipes in Shared Modules
As your application grows, you might have several custom pipes used across multiple modules.
Instead of declaring them repeatedly, create a shared module.
Example:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CapitalizePipe } from './capitalize.pipe';
import { TruncatePipe } from './truncate.pipe';
@NgModule({
declarations: [CapitalizePipe, TruncatePipe],
exports: [CapitalizePipe, TruncatePipe],
imports: [CommonModule]
})
export class SharedPipesModule {}
Now you can import SharedPipesModule
in any feature module.
Best Practice:
✔ Group custom pipes in a Shared Module to improve code reusability and structure.
9. Optimize Performance in Large Applications
When working with large data sets, even pure pipes can become performance bottlenecks if used incorrectly.
Tips to improve performance:
- Use trackBy with
*ngFor
to prevent unnecessary re-rendering.<li *ngFor="let item of items | async; trackBy: trackByFn"> {{ item.name }} </li>
- Avoid impure pipes in performance-critical components.
- Cache transformed values if the data doesn’t change often.
- Use ChangeDetectionStrategy.OnPush to minimize change detection cycles.
Best Practice:
✔ Profile and optimize pipe-heavy templates.
✔ Use OnPush
strategy with AsyncPipe
for maximum efficiency.
10. Testing Pipes
Pipes are simple to test since they are pure functions. You can write isolated unit tests to verify their transformations.
Example test for CapitalizePipe
:
import { CapitalizePipe } from './capitalize.pipe';
describe('CapitalizePipe', () => {
const pipe = new CapitalizePipe();
it('should capitalize the first letter', () => {
expect(pipe.transform('angular')).toBe('Angular');
});
it('should return empty string for null input', () => {
expect(pipe.transform(null as any)).toBe('');
});
});
Best Practice:
✔ Write unit tests for custom pipes.
✔ Keep tests focused on transformation correctness.
11. Avoid Overuse of Pipes in Complex Views
While pipes improve readability, using too many of them in large templates can impact both performance and maintainability.
For example, deeply nested templates with multiple | async
and formatting pipes can become confusing.
Solution:
- Move part of the logic to the component.
- Use computed Observables or preformatted values.
Example:
Instead of:
<p>{{ userData$ | async | json }}</p>
You can process it in the component:
this.formattedUserData$ = this.userData$.pipe(map(data => JSON.stringify(data)));
Then:
<p>{{ formattedUserData$ | async }}</p>
This approach keeps templates lightweight and easier to debug.
Best Practice:
✔ Avoid overusing pipes. Use them for readability, not complexity.
12. Keep Naming Consistent and Meaningful
Always name your pipes according to their purpose.
Examples:
capitalize
truncate
filterActive
formatDate
This makes your templates self-explanatory.
Bad naming examples:
pipe1
customPipe
tempFormat
Best Practice:
✔ Use clear, descriptive pipe names that reflect their function.
Leave a Reply