Entendendo o pure e standalone do Angular Pipe

Veja as diferenças de valores nos parâmetros pure e standalone de um Angular Pipe. Aprenda a usá-los para melhorar o desempenho e a reutilização.

Banner de divulgação do texto. Imagem com diversos canos no fundo e o texto "Angular Pipe - Entenda os parâmetros pure e st
Entenda os parâmetros pure e standalone do Angular Pipe

Em Angular, um "pipe" é um recurso que permite transformar dados antes de serem exibidos na interface do usuário. Por exempo, pode ser utilizado para formatar valores tornando a exibição do dado mais legível. Também aumenta o reaproveitamento de código e traz vantagens em performance.

Por aqui este assunto já foi abordado, então neste texto gostaria de aprofundar alguns detalhes sobre os pipes, falar sobre o parâmetro pure, standalone e construir alguns exemplos falando um pouco sobre value type e reference type do JavaScript.

Criando ambiente de estudo

Os exemplos criados neste texto foram executados com as seguintes configurações do Node.js e Angular:

$ ng version

Angular CLI: 15.2.4
Node: 18.14.2
Package Manager: npm 9.5.0
OS: darwin arm64
versões utilizadas

Para criar o projeto:

$ ng new pipes --skip-tests --skip-git
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS

Para testar o projeto recém-criado:

$ cd pipes
$ npm run start

Agora que o ambiente está configurado vamos falar um pouco sobre os pipes.

Um pouco sobre os pipes

Um pipe é um código que você pode usar para transformar o valor antes de exibi-lo na tela. Normalmente é utilizado direto nos templates para transformar valores, por exemplo, formatar um valor numérico para uma representação monetária.

A síntaxe para utilizar um pipe é o caractere |. Ele é adicionado após o valor que deseja-se transformar, por exemplo, {{ 'OLÁ' | lowercase }} será transformado para olá. Quando o pipe tem argumentos podemos informá-los após a declaração do pipe separando por :, por exemplo: {{ 10 | currency : 'USD' }}.

Os pipes podem ser encadeados para realizar várias transformações em sequência, por exemplo, {{ [0,1,2,3,4,5] | slice : 1 : 3 | json }} será transformado para [ 1, 2 ]. Neste exemplo o pipe slice pegou uma parte do array de entrada, resultando no array [1, 2] e este resultado passou pelo pipe json para transformar este array em uma string (JSON).

<!-- o código a seguir... -->
<div>{{ 50 | currency : 'BRL' }}</div>
<div>{{ 50 | currency : 'BRL' | lowercase }}</div>
<div>{{ 'OLÁ' | lowercase }}</div>
<div>{{ [0,1,2,3,4,5] | slice : 1 : 3 | json }}</div>

<!-- ...renderiza o seguinte conteúdo: -->
<div>R$50.00</div>
<div>r$50.00</div>
<div>olá</div>
<div>[ 1, 2 ]</div>
Exemplos de utilização do Angular Pipe

Exemplo - construindo o FiltroPipe

Agora que falamos o básico sobre os pipes, vamos partir para uma implementação. Vamos criar um pipe que irá filtrar uma lista de nomes. A regra é simples, o pipe deve retornar uma lista filtrada apenas com os nomes que iniciam com algum valor, por exemplo, "filtre os nomes que começam com ab".

Para construir um pipe temos que implementar a interface PipeTransform. Então podemos escrever o seguinte código:

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

export class FiltroPipe implements PipeTransform {
  /**
   * Filtra os itens de uma lista que começam com uma determinada
   * sequência de caracteres
   *
   * @param listaDeNomes Array de string
   * @param filtro Utilizado para filtrar os itens
   * que começam com o valor deste parâmetro
   *
   * @returns Lista filtrada
   */
  transform(listaDeNomes: string[], filtro: string) {
    console.log('FiltroPipe');
    filtro = filtro.toLowerCase();
    return listaDeNomes.filter((a) =>
      a.toLowerCase().startsWith(filtro)
    );
  }
}
filtro.pipe.ts

Na sequência temos o módulo que declara e exporta o pipe acima:

import { NgModule } from '@angular/core';

import { FiltroPipe } from './filtro.pipe';

@NgModule({
  exports: [FiltroPipe],
  declarations: [FiltroPipe],
})
export class FiltroModule {}
filtro.module.ts

Para utilizar o FiltroPipe basta importar o módulo FiltroModule no módulo que declara o componente que utilizará o filtro. No nosso exemplo será o AppModule que contém o AppComponent:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FiltroModule } from './filtro.module';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, FormsModule, FiltroModule],
  bootstrap: [AppComponent],
})
export class AppModule {}
app.module.ts

Observação: importei o FormsModule para utilizar o [(ngModel)] no template (app.component.html).


No AppComponent declaramos uma lista de nomes e uma variável filtro. O objetivo é exibir a lista e permitir que o usuário execute um filtro:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  filtro = '';
  lista = [
    'Benson',
    'Byers',
    'Case',
    'Donaldson',
    'Faulkner',
    'Gaines',
    'Holloway',
    'Huffman',
    'Newton',
    'Ramsey',
    'Rollins',
    'Schroeder',
    'Shields',
    'Soto',
    'Stevenson',
    'Tyler',
    'Williams',
  ];
}
app.component.ts
<div class="container mt-3">
  <div class="row">
    <div class="col">
      <form class="row row-cols-lg-auto g-3 align-items-center">
        <div class="col-12">
          <!--
            Conforme o usuário digitar o valor
            a variável "filtro" é atualizada.
            Por isso o [(ngModel)]="filtro"
          -->
          <input
            class="form-control"
            type="text"
            name="filtro"
            [(ngModel)]="filtro"
          />
        </div>

        <!--
          Este botão apenas dispara um evento
          para que o Angular execute seu ciclo
          de detecção de mudanças.
          Ao longo do texto há mais detalhes.
        -->
        <div class="col-12">
          <input
            class="btn btn-primary"
            type="button"
            (click)="(0)"
            value="Dispara um evento qualquer"
          />
        </div>
      </form>
    </div>
  </div>
  <div class="row mt-3">
    <div class="col-12">
      <table class="table">
        <!--
            Renderiza a lista filtrando os registros
            se baseando no que o usuário digitou no
            input mais acima
          -->
        <tr *ngFor="let item of lista | filtrarPalavras : filtro">
          <td>{{ item }}</td>
        </tr>
      </table>
    </div>
  </div>
</div>
app.component.html

Resultado:

Navegador exibindo uma lista sendo filtrada conforme o usuário digita em um input
FiltroPipe em funcionamento

Agora que construímos um pipe funcional, vamos falar sobre o standalone e pure, que são parâmetros na declaração do @Pipe({}).

standalone

Este recurso foi introduzido no Angular 14 e tem por objetivo eliminar a necessidade de declarar o componente ou pipe em um módulo (NgModule). Antes deste recurso era necessário declarar um componente ou pipe obrigatoriamente em um módulo e eventualmente este módulo só continha uma única declaração. Este padrão é conhecido como SCAM - Single Component Angular Module e é exatamente o que fizemos com o FiltroModule (mais acima).

O standalone dispensa a necessidade de declararmos um módulo exclusivo para um componente ou pipe, então no nosso exemplo podemos fazer o seguinte:

  • excluir o FiltroModule
  • incluir o parâmetro standalone: true no FiltroPipe
  • alterar o AppModule para importar o FiltroPipe ao invés do FiltroModule
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filtrarPalavras',
  standalone: true,
})
export class FiltroPipe implements PipeTransform {
  /* ...(trecho ocultado)... */
}
filtro.pipe.ts

No AppModule retiramos o FiltroModule e colocamos o FiltroPipe:

/* ...(trecho ocultado)... */

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, FormsModule, FiltroPipe],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
app.module.ts
Angular pipes marked as standalone do not need to be declared in an NgModule. Such pipes don't depend on any "intermediate context" of an NgModule (ex. configured providers).

https://angular.io/api/core/Pipe

pure

O parâmetro pure recebe um valor booleano que por padrão é true. Este valor tem relação com a frequência com que a transformação de dados é realizada. Por exemplo, um "pipe puro" é executado apenas quando há uma mudança nos dados de entrada, já o "impuro" é executado sempre que o ciclo de detecção de mudanças do Angular é ativado.

Quando ocorre algum evento que possa atualizar seu data model o CD entra em cena. Por padrão os eventos que disparam este processo são:

todos os eventos do navegador (click, mouseover, keyup, etc.)
setTimeout() e setInterval()
Requisições HTTP (Ajax)

https://consolelog.com.br/como-funciona-change-detection-angular/

No nosso exemplo não declaramos o parâmetro pure, portanto o valor assumido é true. Desta forma o método transform (dentro do FiltroPipe) só será executado caso algum parâmetro (do método transform) seja alterado. Então conforme o usuário digitar no input, o valor da variável filtro será modificado, portanto o transform será chamado e emitirá uma mensagem no console confome o GIF abaixo:

Navegador exibindo uma lista que é filtrada conforme o usuário digita em um campo de texto. No console do DevTools aparecem 3 mensagens indicando que o método transform do FiltroPipe foi executado
Analisando o FiltroPipe com o parâmetro pure: true

Veja que o evento onblur (ocorre quando o input de filtro tem o foco e o usuário clica em outro lugar fazendo com que o foco saia do input) e o clique no botão "Dispara um evento qualquer" não dispara a execução do método transform do FiltroPipe. Isto ocorre porque não houveram alterações na lista e filtro, que são os valores passados para o FiltroPipe.

Alterando o valor pure: true para pure: false no FiltroPipe temos o seguinte resultado:

Navegador exibindo uma lista que é filtrada conforme o usuário digita em um campo de texto. No console do DevTools aparecem 5 mensagens indicando que o método transform do FiltroPipe foi executado
Analisando o FiltroPipe com o parâmetro pure: true

Veja que quando o usuário digita algo no input o transform  do FiltroPipe é executado, exatamente como antes, porém quando disparamos algum outro evento não relacionado aos parâmetro que o FiltroPipe recebe, por exemplo, clicando em um botão ou apenas disparando o evento onblur, ainda assim o código do pipe é executado, ou seja, sempre que ocorre a execução de detecção de mudanças do Angular.


Observação: os exemplos acima foram executados em modo production. Executando os testes em desenvolvimento você perceberá uma duplicidade nas mensagens printadas no console devido a dupla checagem que o Angular faz durante o ciclo de detecção de mudanças.

This guarantee comes at the expense of Angular always running change detection twice, the second time for detecting this type of cases. In production mode change detection is only run once.

https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/

Após entender a diferença de comportamento entre o "puro" e "impuro" você pode se perguntar onde deveria utilizar um pipe pure: false. Bom, em alguns cenários um pipe "impuro" pode ser útil, por exemplo, em um filtro envolvendo a data corrente ou requisições à APIs. Nestes dois casos os parâmetros de entrada são exatamente os mesmos, mas podem produzir resultados diferentes.

@Pipe({
  name: 'excluirRegistrosAnteriorADataCorrente',
  pure: false,
  standalone: true,
})
export class ExcluirRegistrosAnterioresADataCorrente
  implements PipeTransform
{
  transform(registros: Array<{ data: Date }>) {
    const dataCorrente = new Date();
    return registros.filter((a) => a.data > dataCorrente);
  }
}
Cenário onde faz sentido utilizar um pipe com o pure: false

Resumindo, os pipes "puros" são mais recomendados porque podem aproveitar o cache de resultados do Angular, reduzindo a frequência de cálculos e, portanto, melhorando o desempenho do aplicativo. Os pipes "impuros" (pure: false) podem ser úteis em situações em que é necessário atualizar a exibição com frequência, mas deve-se usá-los com cuidado para não prejudicar a performance do aplicativo.

When true, the pipe is pure, meaning that the transform() method is invoked only when its input arguments change. Pipes are pure by default.

https://angular.io/api/core/Pipe

Atenção com primitive type vs reference type

Em JavaScript temos os tipos primitivos e não primitivos. Os tipos string, number, bigint, boolean, undefined, symbol e null são tipos primitivos. Isto significa que o valor é armazenado diretamente na variável. Por exemplo, quando você atribui uma string a uma variável, o valor da string é armazenado na posição de memória associada a essa variável. Quando você atribui uma nova string a essa mesma variável, a posição de memória é atualizada com o novo valor da string.

All primitives are immutable; that is, they cannot be altered. It is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned to a new value, but the existing value can not be changed in the ways that objects, arrays, and functions can be altered. The language does not offer utilities to mutate primitive values.

https://developer.mozilla.org/en-US/docs/Glossary/Primitive
const valor1 = 'olá';
const valor2 = 'olá';

console.log(Object.is(valor1, valor2)); // true
console.log(valor1 === valor2); // true
Exemplo

Já no caso dos tipos não primitivos, como objetos, arrays e funções, o valor é armazenado por referência. Isso significa que, em vez de armazenar o valor diretamente na variável, o JavaScript armazena um ponteiro para a posição de memória onde o valor está armazenado. Quando você atribui uma nova referência a uma variável, a posição de memória associada a essa variável é atualizada com o novo ponteiro para o objeto. Isso significa que, mesmo que você atribua uma nova referência a uma variável, o objeto original ainda existe na memória e pode ser acessado por outras variáveis que o referenciam.

// Case 1: Evaluation result is the same as using ===
Object.is(25, 25); // true
Object.is("foo", "foo"); // true
Object.is("foo", "bar"); // false
Object.is(null, null); // true
Object.is(undefined, undefined); // true
Object.is(window, window); // true
Object.is([], []); // false
const foo = { a: 1 };
const bar = { a: 1 };
const sameFoo = foo;
Object.is(foo, foo); // true
Object.is(foo, bar); // false
Object.is(foo, sameFoo); // true

// Case 2: Signed zero
Object.is(0, -0); // false
Object.is(+0, -0); // false
Object.is(-0, -0); // true

// Case 3: NaN
Object.is(NaN, 0 / 0); // true
Object.is(NaN, Number.NaN); // true

// fonte:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is

Trazendo as considerações acima para a construção de um pipe, veja como o pure pode modificar o comportamento do pipe diante dos tipos primitivos e não primitivos:

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

@Pipe({
  name: 'toJson',
  standalone: true,
  pure: true,
})
export class ToJsonPipe implements PipeTransform {
  transform(valor: any): string {
    const json = JSON.stringify(valor);
    console.log(`transform:ToJsonPipe: ${json}`);
    return json;
  }
}
to-json.pipe.ts

Importando o ToJsonPipe no AppModule podemos utilizá-lo no template:

<div class="container mt-3">
  <div class="row mb-3">
    <div class="col-12">itens: {{ itens | toJson }}</div>
    <div class="col-12">valor: {{ valor | toJson }}</div>
    <div class="col-12">
      <button
        class="btn btn-primary me-2"
        (click)="manterValor()">
        Manter item
      </button>
      <button
        class="btn btn-primary"
        (click)="alterarValor()">
        Alterar valor
      </button>
    </div>
  </div>
</div>
app.component.html

Já na classe temos dois métodos:

  1. alterarValor: altera uma propriedade de itens e o valor da variável valor
  2. manterValor: mantém o valor da variável valor como 5 e uma propriedade de itens com seu valor original declardo no escopo da função.
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  itens: { [key: string]: any } = {
    item1: { cor: '#000', id: 1 },
    item2: { cor: '#aaa', id: 2 },
    item3: { cor: '#fff', id: 3 },
  };

  valor = 5;

  alterarValor() {
    this.valor = Math.random();
    this.itens['item1'].cor = Math.random();
  }

  manterValor() {
    this.valor = 5;
    this.itens['item1'].cor = '#000';
  }
}
app.component.ts

Veja no template que estamos aplicando o toJson em um valor numérico (tipo primitivo) e um objeto chamado itens (não primitivo). Observe o resultado considerando que estamos utilizando o pure: true:

Navegador exibindo um JSON, um valor número e dois botões: "Manter item" e "Alterar valor". O usuário clica no primeiro botão e depois no segundo. Ao clicar nos botões aparecem mensagens no console do DevTools.
Exemplo do funcionamento do ToJsonPipe com pure: true

É possível notar que o valor da variável valor é atualizado na tela sempre que recebe um novo valor, já que estamos utilizando o pure: true. O mesmo não ocorre quando alteramos uma propriedade do objeto itens. O motivo é que em JavaScript um object é tratado como um tipo não primitivo, ou seja, um tipo referenciado. Então quando passamos o itens para o ToJsonPipe, no fundo estamos passando um poteiro, um endereço de memória que aponta para o conteúdo do object. Quando o Angular compara os valores, identifica que não houve mudanças neste ponteiro, ou seja, ele ainda aponta para o mesmo objeto, portanto não atualiza o valor na tela. Para testar esta última afirmação você pode clonar o objeto itens e verá que agora o transform do seu pipe irá atualizar os valores da expressão {{ itens | toJson }}:

alterarValor() {
  this.valor = Math.random();
  this.itens['item1'].cor = Math.random();

  // cria um novo objeto clonado
  this.itens = { ...this.itens };
 }
trecho do app.component.ts

Se optarmos por utilizar o pure: false, como sabemos, o Angular irá sempre executar a função transform quando houver um ciclo de atualização da tela:

Navegador exibindo um JSON, um valor número e dois botões: "Manter item" e "Alterar valor". O usuário clica no primeiro botão e depois no segundo. Ao clicar nos botões aparecem mensagens no console do DevTools.
Exemplo do funcionamento do ToJsonPipe com pure: false

Então sempre tome cuidado ao optar por um valor no parâmetro pure do seu pipe.

Considerações

Um pipe é uma ferramenta útil em Angular que permite formatar e transformar dados de forma fácil e consistente. Ajuda na performance e no reaproveitamento de código. O uso correto do parâmetro pure irá ajudar a tomar a melhor decisão levando em consideração performance, tipo do dado e frequência de atualização.

Também falamos sobre o standalone e um pouco sobre o uso de dados primitivos e não primitivos. Deixo como sugestão analisar os pipes que o Angular disponibiliza nativamente. Esta lista de pipes pode ser encontrada neste link.

Links relacionados ao conteúdo abordado: