Avoid Duplicate Function Calls in Angular Template using Memoizer

Angular provides ways to call functions directly from HTML code. This makes development easier, but it can sometimes cause performance issues. I will explain how to avoid duplicate function calls in the Angular template using a memoizer.

A memoizer is a pure pipe we use to cache the function results. The function is called only if the parameters are changed otherwise it avoids the call.

Avoid duplicate function calls in Angular using a memoize pipe

There are several ways to avoid duplicate calls in Angular. However, in this article, I will focus on how to prevent it using a memoize pipe.

Do you know how many ways are there to share data between components in Angular?

Create a memoize pure pipe

The first step is to create a pure pipe in Angular and name it as memoize. We will take three parameters in the function- a) handler function, b) current context, and c) arguments you want to track for changes.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'memoize',
  standalone: true,
})
export class MemoizePipe implements PipeTransform {
  public transform<Args extends unknown[], R>(handler: (...args: Args) => R, context?: unknown, ...args: Args): R {
    return handler.call(context, ...args);
  }
}

This pure pipe will process a value or array of values using a function provided by the user. It will also provide the context data for the function call. If context is not provided then it will call the function with undefined context.

Consume the memoize pipe in the Angular template

Now that our pipe is created, let’s call it from the Angular templates.

app.component.scss

.container {
    padding: 20px;
    max-width: 800px;
    margin: 0 auto;
}

section {
    margin: 20px 0;
    padding: 15px;
    border: 1px solid #ddd;
    border-radius: 4px;
}

h2 {
    color: #333;
    margin-bottom: 20px;
}

h3 {
    color: #666;
    margin-bottom: 15px;
}

p {
    margin: 10px 0;
    padding: 5px;
    background: #f5f5f5;
    border-radius: 3px;
}

app.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MemoizePipe } from './memoize.pipe';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, MemoizePipe],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
  public sqDigit: number = 0;

  public square(num: number): void {
    this.sqDigit = num * num;
  }

  public hasValue(value: number): boolean {
    console.log('hasValue called');
    return !!value;
  }
}

Let’s call the memoize pipe with hasValue handler and provide the current context this along with a single parameter sqDigit.

app.component.html

<main class="main container">
  <section (mouseover)="square(5)">
    <h3>Hover over here</h3>
    <p>
      Square of 5: {{ sqDigit }}
    </p>

    <p *ngIf="hasValue | memoize: this: sqDigit">
      It has value
    </p>
  </section>
</main>

Run the angular application and open it in the browser. We can see the hasValue called log is printed only once. I hover on the element but the function is not called more than once.

Now let’s remove the memoize pipe and call the function directly from the template.

<main class="main container">
  <section (mouseover)="square(5)">
    <h3>Hover over here</h3>
    <p>
      Square of 5: {{ sqDigit }}
    </p>

    <p *ngIf="hasValue(sqDigit)">
      It has value
    </p>
  </section>
</main>

The function is called infinite times when we hover over the element if we don’t use the memoize pipe.

Pass multiple arguments in the memoize pipe for the handler function

We can also pass various arguments of different types to the memoize pipe for the handler function if it expects more than one parameter. Here is an example of how to do it.

Create another variable of a different data type. I used anotherValue with boolean type. I have passed the variable in hasValue function so that I can send it to memoize pipe with rest parameter.

app.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MemoizePipe } from './memoize.pipe';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, MemoizePipe],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
  title = 'stackblogger';

  public sqDigit: number = 0;
  public anotherValue: boolean = false;

  public square(num: number): void {
    this.sqDigit = num * num;
    this.anotherValue = true;
  }

  public hasValue(value: number, anotherValue: boolean): boolean {
    console.log('hasValue called');
    return !!value && anotherValue;
  }
}

Pass the arguments from template.

app.component.html

<main class="main container">
  <section (mouseover)="square(5)">
    <h3>Hover over here</h3>
    <p>
      Square of 5: {{ sqDigit }}
    </p>

    <p *ngIf="hasValue | memoize: this: sqDigit: anotherValue">
      It has value
    </p>
  </section>
</main>

Save the code and run the application. In the output window, you will see that the function is called only once.

hasValue function is called only once even with mouse hover.

Conclusion

We always choose the easy way during code development but do not notice that it will take longer than the initial implementation to fix the performance issues in future.

Always avoid duplicate function calls in the Angular template using either a memoizer pipe or writing a custom pipe for the task itself.

Check out my article on the ForkJoin RxJs operator and its real-world use cases.

Leave a Reply

Your email address will not be published. Required fields are marked *