Deep dive into Angular pipes
Tags: angular
Published: 2023-02-28 08:10:00

The aim of this article is to explain everything related to pipes. First, we will learn how to build a pipe, and then we will explore the benefits of using them. We will dive into the Angular source code to have a better understanding of how they work and what their use cases are.

This article will also provide the solution for [challenge #8](https://github.com/tomalaforge/angular-challenges/blob/main/apps/pipe-easy/README.md) of Angular Challenges. This challenge has been designed for beginners to get their first look into pipes. If you haven't tried it yet, I encourage you to do so before coming back to compare your solution with mine. _(You can also submit a PR that I'll review)_

---

In the challenge, we start with the following code snippet:

```typescript
@Component({
  standalone: true,
  imports: [NgFor],
  selector: 'app-root',
  template: `
    <div *ngFor="let person of persons; let index = index">
      {{ heavyComputation(person, index) }}
    </div>
  `,
})
export class AppComponent {
  persons = ['toto', 'jack'];

  heavyComputation(name: string, index: number) {
    // very heavy computation
    return `${name} - ${index}`;
  }
}
```
We have a simple for loop that iterates over an array of person's name, and for each person, we call the `heavyComputation` function.

> _In this example, the function simulates a heavy computation, but in reality it could be a costly calculation such as filtering an array._

In Angular project, it is common to see function calls inside templates because it is the most natural way to get information from our component into our template. However, you may  have heard the following phrase: "You should never use function calls!". This statement is often true because your function will be recompute at each change detection cycle, and Angular can run many of those cycles.

That said, calling functions inside a template is not forbidden or bad. If we only want to retrieve an attribute from an object, as in the following example, the cost is very low, so calling this function is totally acceptable.

```typescript
getFirstname = (index: number) => person[index].firstname
```
Functions can be easily modified to perform heavier calculation without considering the impact of the modification, which can harm the performance of your application.

That is why, we should use pipes inside our template to transform our data. Pipes are memoized, meaning if none of the inputs have changed , the last computed value will be returned. Otherwise the `transform` function of our pipe will be re-executed.

Let's modify our code to add a pipe to our template:

```typescript
@Pipe({
  name: 'comput',
  standalone: true,
  pure: true // default value
})
export class ComputPipe implements PipeTransform {
  transform(name: string, index: number): string {
    // very heavy computation
    return `${name} - ${index}`;
  }
}
```
and our template become:
```html
<div *ngFor="let person of persons; let index = index">
  {{ person | comput: index }}
</div>
```
**Note:** 
- The syntax of a pipe is as follow:
```
var | pipeName : arg1: arg2: arg3
```

- We can concatenate multiple pipes together
```
var | pipe1 | pipe2
```

The argument `var` will be transformed by `pipe1` first and then the result will be transformed by `pipe2`.

- By default, a pipe is **pure**. This means that Angular will memorize the result of the first execution and will re-evaluate the pipe only if one or more inputs change.

**Note:** A pure pipe **must** use a pure function meaning that the function should not trigger any side effects. The function should return the same output for the same input.

We can also create **impure** pipe by setting `pure: false` inside the pipe decorator. This means that the pipe function will be executed at each change detection cycle. However this property should be used with caution since it can harm your application's performance if your function is costly. Using an impure pipe is similar to calling a function from a template, but it allows you to reuse the function anywhere in your application.

For example, the built-in `AsyncPipe` is an impure pipe since it needs to re-evaluated the observable at each change detection cycle to refresh the view.

- One last thing that is often forgotten is that the decorator `@Pipe` creates an injectable element, which means that we can import the pipe into any component, directive or service and call its `transform` method.

If we take the previous pipe example, we could write something like that:

```typescript
@Injectable()
export class MyService {
  // we can inject the pipe
  computPipe = inject(ComputPipe);

  doSomething(){
    return this.computPipe('xxx', 1);
  }
}
```

---

We now know how to create a pipe and have some general understanding of it. Let's dive into Angular's source code to better understand how a pipe is implemented under the hood.

Before we look at some code snippets, we need to understand how Angular processes our template information. We need a very basic introduction to  what an `LView` and `TView` are.

Internally, Angular transforms all of our template information into `LView` _(Logical View)_ and `TView` _(Template View)_.

- `TView` represents the compiled template of a component or pipe and contains information about the structure and content of the template. It contains information about the directives, bindings, elements, template's metadata, styles,… When Angular compiles a component, its template is transformed into a `TView` object. `TView` contains static data needed to efficiently  render a template and its instance can be shared among any `LView` that uses that particular component.

- `LView` is a data structure that represents the state of a component and its associated template. It contains information about the component's properties, methods, bindings and also the state of the template. This object is used by the Angular runtime and is updated whenever the component's properties or state change.

If we take the view of our example above: 

```html
<div *ngFor="let person of persons; let index = index">
  {{ person | comput: index }}
</div>
```
A `TView` is created for the `AppComponent` and for the `ComputPipe`.

A `LView` is created for the `AppComponent` and also a `LView` is created for each div of our `*ngFor` loop. _(Since the persons array is of length 2 in our example, we have a total of 3 `LView` as shown below)_

```bash
LViewComputPipe_toto = LView{
  TViewComputPipe,
  // ...
  // state information
  // ...
}

LViewComputPipe_jack = LView{
  TViewComputPipe,
  // ...
  // state information
  // ...
}

LViewAppComponent = LView{
  TViewAppComponent,
  LViewComputPipe_toto,
  LViewComputPipe_jack,
  // ...
  // state information
  // ...
}
```

This is a very simplified version of `LView`, but it's all you need to understand for the following section. Let's go though the code to see how pipe are implemented when a new change detection cycle is triggered.

Depending on the number of arguments, the function being called for our pipe is `ɵɵpipeBind[number of arg]` which is `ɵɵpipeBind2` in our case since comput takes two arguments: `persons` and `index` . Thus at each change detection cycle, the function below is executed for each of our pipe.

```typescript
function ɵɵpipeBind2(index, slotOffset, v1, v2) {
    const adjustedIndex = index + HEADER_OFFSET;
    const lView = getLView();
    const pipeInstance = load(lView, adjustedIndex);
    return isPure(lView, adjustedIndex) ?
        pureFunction2Internal(lView, getBindingRoot(), slotOffset, pipeInstance.transform, v1, v2, pipeInstance) :
        pipeInstance.transform(v1, v2);
}
```
- index, HEADER_OFFSET and slotOffset are used to locate the slots of the required element within the `LView` object. In the case of the pipe, we want the state of each argument, the result of the `transform` method and the pipe instance.
- v1 and v2 are the two input  arguments.

If we examine the slots that are most relevant to us within the `LView`, we can see the following:

```bash
lView = {
// ...
24: ComputPipe {}
// ...
26: {__brand__: 'NO_CHANGE'}
27: {__brand__: 'NO_CHANGE'}
28: {__brand__: 'NO_CHANGE'}
// ...
}
```
- slot 24 stores the instance of the pipe
- slot 26 stores the value of arg1
- slot 27 stores the value of arg2
- slot 28 caches the result of the `transform` method

**Note:** This `LView` shows the initialization state.

To begin, we must determine whether the pipe is pure using the following function:

```typescript
function isPure(lView: LView, index: number): boolean {
  return (<PipeDef<any>>lView[TVIEW].data[index]).pure;
}
```
The constant `TVIEW = 1` stores the `TView` slot inside the `LView`. Since all static data is stored inside the `TView`, we can retrieve all metadata relating to this pipe _(As shown below)_

```bash
{
  factory: ƒ ComputPipe_Factory(t),
  name: "comput",
  onDestroy: null,
  pure: true,
  standalone: true,
  type: class ComputPipe
}
```
If the pipe is impure, we simply return the `pipe.transform` method and re-execute the entire function.

However, in most cases, the pipe will be pure and we will call `pureFunction2Internal`.

```typescript
export function pureFunction2Internal(...): any {
  const bindingIndex = bindingRoot + slotOffset;
  return bindingUpdated2(lView, bindingIndex, exp1, exp2) ?
      updateBinding(
          lView, bindingIndex + 2,
          thisArg ? pureFn.call(thisArg, exp1, exp2) : pureFn(exp1, exp2)) :
      getPureFunctionReturnValue(lView, bindingIndex + 2);
}
```
The `pureFunction2Internal` method needs to validate a new condition. First, we must compare the two new input arguments with the ones stored inside the `LView`. The `bindingUpdated2` is called to archive this:

```typescript
export function bindingUpdated2(lView: LView, bindingIndex: number, exp1: any, exp2: any): boolean {
  const different = bindingUpdated(lView, bindingIndex, exp1);
  return bindingUpdated(lView, bindingIndex + 1, exp2) || different;
}
```

```typescript
export function bindingUpdated(lView: LView, bindingIndex: number, value: any): boolean {
  const oldValue = lView[bindingIndex];

  if (Object.is(oldValue, value)) {
    return false;
  } else {
    lView[bindingIndex] = value;
    return true;
  }
```
The `bindingUpdated` method compares the stored value of `arg1` with the current value using the `Object.is` method. If both value are different, we update the `LView` with the new argument.

> _`Object.is` is quite similar to `===` except that -0 !== 0 and Number.NaN === NaN_

We run `bindingUpdated` for `arg2` as well and we sum both results.

If all of the pipe's arguments are identical to the previous change detection cycle, we return the cached value by calling `getPureFunctionReturnValue`:

```typescript
function getPureFunctionReturnValue(lView: LView, returnValueIndex: number) {
  const lastReturnValue = lView[returnValueIndex];
  return lastReturnValue === NO_CHANGE ? undefined : lastReturnValue;
}
```
Otherwise, we run the `transform` method and we save the result inside the `LView` using the `updateBinding` function:

```typescript
export function updateBinding(lView: LView, bindingIndex: number, value: any): any {
  return lView[bindingIndex] = value;
}
```
After one cycle of change detection, we can see that the `LView` contains our new state values:

```bash
lView = {
// ...
24: ComputPipe {}
// ...
26: "toto"
27: 0
28: "toto - 0"
// ...
}
```
Now if both arg don't change, the `transform` method of the pipe will not be called and the cached value _(slot 28)_ will be returned. However if one or both arguments change, the pipe will be re-executed.

**Note:**
- If your pipe function has one argument or more than two, the logic is strictly identical. All arguments will be checked in order to determine if the pipe needs to be recalculated.
- If you have read this excellent article from Enea Jaholli _(link below)_, we can argue about using a `memo` function to wrap our function inside our component instead of using a pipe. However the example above will not work with this approach. The `memo` function will only work efficiently if you call your function with the same set of arguments because there is only one 'instance' of the function. _(For example, if you call your function with the argument 'toto', 'toto' will be memoized. Then you call the same function with 'titi', since 'toto'!='titi', the function will rerun. At the next change detection cycle, you will run the function with 'toto' again and even if the argument is the same, the function will get re-executed since the memoized value is now 'titi' and so on)_. In comparison, pipes have their own `LView` and thus their own instance. So you can have multiple identical pipes inside the same template with different arguments and still leverage the cached algorithm of Angular.

{% embed https://itnext.io/its-ok-to-use-function-calls-in-angular-templates-ffdd12b0789e %}

---

I hope that pipe has no more secrets and you will use it efficiently in your application.

I hope you learned new Angular concept. If you liked it, you can find me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge).