Understanding Pipe Instantiation Life-Cycles In Angular 4.2.3
The other day, I was building a Pipe class in Angular 4 when it occurred to me that I had never created a Pipe that used a constructor() method. This then made me realize that I didn't truly understand the instantiation life-cycle for Pipes, both pure and impure; nor did I have a good sense of what kind of "state" may or may not be safe to set in the constructor() method. As such, I wanted to do a quick debugging exploration to see when and how often Pipe classes get created in an Angular component.
Run this demo in my JavaScript Demos project on GitHub.
NOTE: This is not an exploration of how often the pipe's transform() method runs - only how often the Pipe class itself gets instantiated.
To test this, I first created two pipes that do exactly the same thing: output the string-length of their input value. The only difference between the two pipes is their selector name and their purity - one is pure, the other is not:
// Import the core angular services.
import { Pipe } from "@angular/core";
import { PipeTransform } from "@angular/core";
@Pipe({
name: "length",
pure: true
})
export class LengthPipe implements PipeTransform {
// I initialize the length pipe.
constructor() {
console.warn( "Creating Length Pipe." );
}
// ---
// PUBLIC METHODS.
// ---
// I return the length of the given value.
public transform( value: string ) : number {
return( value.length );
}
}
... and its impure counterpart:
// Import the core angular services.
import { Pipe } from "@angular/core";
import { PipeTransform } from "@angular/core";
@Pipe({
name: "lengthImpure",
pure: false
})
export class LengthImpurePipe implements PipeTransform {
// I initialize the length pipe.
constructor() {
console.warn( "Creating LengthImpure Pipe." );
}
// ---
// PUBLIC METHODS.
// ---
// I return the length of the given value.
public transform( value: string ) : number {
return( value.length );
}
}
As you can see, both transform() methods do the same thing - return the length of the given string value. The only meaningful part of these two Pipes is that they log their instantiation in the constructor() method. This will allow us to see how often these two types of Pipes get created.
Now, when it comes to creation possibilities for these Pipes, I wanted to test two things:
- Toggle the rendering within the same component.
- Toggle the rendering within a child component.
Testing at these two levels of the component tree should allow us to see what kind of caching is being done based on the kind of Pipe (pure vs. impure). Therefore, my app component will toggle both a normal DIV and a child component, both of which contain the use of both Pipes.
First, let's look at the app component. This has top-level Pipes and Div-level Pipes as well as the child component:
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
selector: "my-app",
styleUrls: [ "./app.component.css" ],
template:
`
<p>
Use message:
<a (click)="useMessage( 'Hello' )">Hello</a> |
<a (click)="useMessage( 'Good-bye' )">Good-bye</a>
</p>
<p>
Pure: {{ message | length }}<br />
Pure: {{ message | length }}<br />
Impure: {{ message | lengthImpure }}<br />
Impure: {{ message | lengthImpure }}<br />
</p>
<!-- This will render a new set of pipes in the SAME COMPONENT. -->
<p>
<a (click)="toggleDiv()">Toggle Div</a>
</p>
<p *ngIf="isShowingDiv">
Pure: {{ message | length }}<br />
Impure: {{ message | lengthImpure }}
</p>
<!-- This will render a new set of pipes in a CHILD COMPONENT. -->
<p>
<a (click)="toggleChild()">Toggle Child</a>
</p>
<my-child
*ngIf="isShowingChild"
[message]="message">
</my-child>
`
})
export class AppComponent {
public isShowingChild: boolean;
public isShowingDiv: boolean;
public message: string;
// I initialize the app component.
constructor() {
this.asyncGroup( "Bootstrapping App Component" );
this.isShowingChild = false;
this.isShowingDiv = false;
this.message = "Hello";
}
// ---
// PUBLIC METHODS.
// ---
// I toggle the rendering of the child component.
public toggleChild() : void {
this.asyncGroup( "Toggle Child Component" );
this.isShowingChild = ! this.isShowingChild;
}
// I toggle the rendering of the div container.
public toggleDiv() : void {
this.asyncGroup( "Toggle Div" );
this.isShowingDiv = ! this.isShowingDiv;
}
// I change the message being used in the template.
public useMessage( message: string ) : void {
this.message = message;
}
// ---
// PRIVATE METHODS.
// ---
// I wrap an asynchronous set of console statements in a group with the given name.
private asyncGroup( name: string, duration: number = 100 ) : void {
console.group( name );
setTimeout( console.groupEnd, duration );
}
}
As you can see, the pure and impure versions of the Pipes are always being executed in pairs.
The child component does nothing but render the pair of pipes based on the bound input value:
// Import the core angular services.
import { Component } from "@angular/core";
@Component({
selector: "my-child",
inputs: [ "message" ],
styleUrls: [ "./child.component.css" ],
template:
`
<p>
Pure: {{ message | length }}<br />
Impure: {{ message | lengthImpure }}
</p>
`
})
export class ChildComponent {
public message: string;
}
Now that we have our tree of components and dynamic containers setup, we can run the application and try to toggle both the Div and the child. And, when we do so, we get the following output:
As you can see, the pure Pipes appear to be cached at the component level. There are two pure pipes in the root of the app component and one in the dynamic Div. And yet, we only ever see one pure Pipe being instantiated in the app component. It's not until we toggle the child component that we actually see another pure Pipe get created. On the flipside, impure Pipes clearly get instantiated for every single use of the pipe in a component template.
This means that an impure Pipe can have unique state for every instance of the Pipe whereas a pure Pipe can only have unique state for every parent context. Now you might try to argue that a "pure" pipe should only be based on the transform() method arguments. But, this is clearly not the case as all Pipes should be able to leverage dependency-injection (which is, by definition, creating state). It's just that, with a pure Pipe, the state should remain unchanged throughout the life-cycle of the Pipe instance.
Good to know - building that Angular mental model!
Want to use code from this post? Check out the license.
Reader Comments