Quand je relis du code Angular fait par des gens qui font principalement du React ou juste qui ne sont pas très au fait des mécaniques internes d'Angular je tombe souvent sur des erreurs du type : utiliser une méthode pour faire un traitement et l'appeler directement dans le template.
C'est une erreur de base mais ça peut créer un gros problème de performance, sans oublié qu'on passe à côté de la possibilité de créer un élément réutilisable : un pipe. Dans certains cas on aurait juste besoin d'utiliser le hook ngOnChanges().
Ce qu'il ne faut pas faire
Je vois souvent ce genre de chose :
// random-thing.component.ts
get foo() {
return { bar: this._foo.toUppercase() };
}
// random-thing.component.html
<span>{{ foo.bar }}
ou bien ça :
// random-thing.component.ts
foo() {
return { bar: this._foo.toUppercase() };
}
// random-thing.component.html
<span>{{ foo().bar }}
C'est une grosse erreur au sens où vous allez avoir un re-rendu en continue de votre composant. À l'inverse de React Angular va chercher à déterminer automatiquement quand il doit re-rendre un composant en se basant sur la référence des valeurs qu'on lui passe. Dans les deux exemples ci-dessus, on fait un traitement qui crée une nouvelle chaîne de caractère et un nouvel objet contenant la chaîne systématiquement car à chaque parcourt de l'arbre de composant pour déterminer s'il faut re-rendre, Angular va appeler notre fonction, trouver en réponse une nouvelle référence et donc re-rendre.
Même sans parler de re-rendu, Angular fonctionne en comparant les états de valeurs bindé sur vos template à chaque fois qu'il se produit un évènement sur votre page. Je vous ne le cache pas, ça arrive très très TRÈS souvent (mettez un console.log dans une fonction comme celle-ci dans une vraie application et commencez à utiliser l'application pour vous en convaincre). Est-ce vraiment nécessaire de refaire certains traitements plusieurs fois par seconde quand on sait d'avance à quel moment le résultat va changer ?
Solution simple : ngOnChanges()
Très souvent quand on crée une fonction qu'on va appeler depuis le template c'est qu'on cherche à exprimer une chose très simple : quand tel objet passé en paramètre change je veux calculer le nouvel affichage. Dans ces cas-là, le plus simple est de passer par le hook ngOnChanges().
Ce hook est très simple : c'est une méthode de nos composants qui sera appelé à chaque fois que nos composants recevront des nouvelles valeurs en Input. Si vous cherchez à réagir au changement d'une valeur qui n'est pas un Input ne passez pas par ngOnChanges(), ça ne fonctionnera pas.
Pour l'écriture c'est très simple :
import { Input, OnChanges, SimpleChanges } from '@angular/core';
...
export class ThingComponent implements OnChanges {
@Input() _foo: string;
foo = '';
ngOnChanges(changes: SimpleChanges) {
if (changes._foo) {
this.foo = changes._foo.currentValue.toUppercase();
}
}
}
Dans cette solution on voit qu'on reçoit simplement en paramètre un objet indiquant les changements qui viennent de se produire sur nos Input. A noter que changes ne contiendra pas tous les Input mais uniquement ceux qui ont changé.
Comme on fige une valeur qui sera ensuite utilisé dans le template, on aura aucun problème de re-rendu ici.
Solution la plus propre : le Pipe
Dans beaucoup de cas on pose une fonction pour sortir du template un formattage pour l'affichage. C'est le cas dans mon exemple.
On veut que l'affichage du texte soit en lettre capitale. Une fonction semble tout indiqué et c'est bien le cas, il manque juste un détail structurel. En fait il vaut mieux passer par un Pipe qui pourra être pur dans la majorité des cas et éviter au maximum les re-rendus intempestifs.
En pratique :
<span>{{ foo() }}</span>
// devient
<span>{{ _foo | upper }}</span>
Dans mon exemple j'utilise un Pipe tout prêt mais si je devais recoder ce Pipe rapidement, j'aurai quelque chose comme ça :
// upper.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'upper' })
export class UpperPipe implements PipeTransform {
transform(value: string) {
return value == undefined ? '' : value.toUppercase();
}
}
Par défaut on crée un Pipe pur (on peut l'indiquer explicitement dans le décorateur avec un pure: true
), ce qui indique a Angular qu'il ne doit appliquer la transformation que si la référence de value actuelle est différente de la précédente, sinon il renvoie directement le résultat du dernier calcul.
C'est très simple à écrire un Pipe, on peut extraire un comportement générique facilement (je pense très vite à un formattage de date ou de nombre spécifique au contexte client ou un objet complexe qui doit être affiché sous une certaine forme par convention).
À noter que même si je les ai omis ici, on peut passer des paramètres à nos Pipe.
Favoriser les Pipe autant que possible
De mon expérience en Angular les Pipe sont des outils très puissants. On peut faire beaucoup de traitement dans un Pipe. C'est un outil très expressif au niveau du template et qui aide à maintenir du single responsibility en extrayant beaucoup de code des composants.
Si on écrit correctement nos Pipe on aura une optimisation automatique des re-rendus, ce qui est très appréciable. Comme notre Pipe dépend de peu de chose, on aura quelque chose de très simple à tester et des tests très rapides à s'exécuter.
Dans certains cas j'utilise aussi des Pipe même si mon comportement n'est pas pur. Pour ceux qui ne seraient pas à l'aise avec le terme "pur" je parle ici de la règle en programmation fonctionnelle qui veut que le résultat d'une fonction ne dépende que de ces paramètres. Créer des Pipe impure permet à défaut d'optimiser les re-rendu d'isoler un comportement. C'est typiquement le cas quand on dépend de l'environnement. J'ai récemment créé un pipe qui faisait un formattage de date différent en fonction de la langue choisi pour l'affichage par l'utilisateur. En sachant qu'au changement de langue on devait changer aussi l'affichage, on est donc dans un comportement impur mais qui a été extrait proprement. À l'usage je suis très content du résultat que j'ai utilisé un peu partout dans l'application :
<span>{{ date | localizedDate:'withTime' }}
Pas besoin d'indiquer la locale ou le format, c'est le pipe qui se charge de tout ici.
Resources :
Crédit photo : https://pixabay.com/photos/person-man-male-worker-inside-731151/