Les signaux Angular introduisent plusieurs fonctionnalités à Angular qui vous permettront de simplifier votre développement et vous aideront à construire des applications plus rapides par défaut.
Dans cet article, nous verrons en détail les nombreuses capacités que possèdent Angular Signals et savoir comment les utiliser correctement.
Avant de se lancer
Vous avez la possibilité de suivre notre formation Angular sur 3 jours durant laquelle vous apprendrez à développer des applications Web interactives écrites en TypeScript, à créer vos propres composants réutilisables et à maitriser les différentes facettes de la technologie.
Si vous avez déjà de l’expérience sur Angular, nous vous proposons notre formation Angular Avancé. Vous apprendrez à résoudre des problèmes d’orchestration sur RxJS, l’amélioration des performances de votre site Web ou développer et gérer des composants Angular.
L’équipe Ambient IT
Qu’est-ce qu’Angular Signals ?
C’est un système qui suit régulièrement comment et où l’état est utilisé dans l’application afin de permettre au framework d’optimiser les mises à jour de rendu. Il y a des valeurs réactives définies dans le but qu’elles puissent exprimer des dépendances entre elles.
Signals
Un signal correspond à un programme dont la fonction principale est d’appeler une autre fonction (wrapper). Cette fonction se situe autour d’une valeur qui indique à l’utilisateur lorsqu’une valeur change. Ces signaux peuvent contenir toutes les valeurs possibles allant des simples primitives aux structures de données complexes. La lecture d’une valeur d’un signal se fait toujours grâce à la fonction getter qui permet à Angular de suivre où est utilisé le signal.
Il existe différents types de signaux, les signaux inscriptibles et les signaux calculés.
Signaux inscriptibles
Les signaux inscriptibles garantissent une API pour mettre à jour les valeurs. Il est possible de créer des signaux inscriptibles en utilisant la fonction signal
:
const count = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());
Afin de changer la valeur d’un signal inscriptible, il est possible grâce au .set
:
count.set(3);
L’opération .update()
permet de calculer une nouvelle valeur grâce à une précédente valeur :
// Increment the count by 1.
count.update(value => value + 1);
Pendant l’utilisation de signaux qui contiennent des objets, il est important de déplacer directement l’objet. Cela signifie qu’il est possible d’ajouter une nouvelle valeur sans avoir à entièrement remplacer la précédente valeur déjà présente. Ce type de modification interne peut se faire en utilisant la méthode .mutate
:
const todos = signal([{title: 'Learn signals', done: false}]);
todos.mutate(value => {
// Change the first TODO in the array to 'done: true' without replacing it.
value[0].done = true;
});
Ces signaux possèdent le type WritableSignal.
Signaux Calculés
Les signaux calculés tirent leur valeur de divers signaux. Pour définir un signal calculé, il suffit d’utiliser computed
et spécifier une fonction de dérivation :
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
Le doubleCount
signal dépend de count
. Lorsque les count
sont mis à jour, le framework Angular sait qu’il faut qu’un signal doubleCount
soit également actualisé.
Angular permet aux calculs d’être mémorisés et d’être évalués. La fonction de dérivation ne s’exécutera pas pour calculer sa valeur tant que doubleCount
n’a pas été lue. Lorsque la valeur est calculée, cette dernière est mise en cache et les prochaines lectures du signal doubleCount
renverront la valeur sans passer par un calcul. Si le count
est amené à changer, il indique au doubleCount
que sa valeur mise en cache n’est plus valide, et qu’elle sera recalculée seulement lors de la prochaine lecture de doubleCount
.
Il est important de savoir que les signaux calculés et les signaux inscriptibles sont totalement différents. Il est impossible d’affecter directement des valeurs à un signal calculé, cela produira une erreur de compilation, car doubleCount
n’est pas un WritableSignal.
Les dépendances de signals calculés sont dynamiques.
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});
Cet exemple montre que le count
signal n’est lu que de manière conditionnelle. Pendant la lecture de conditionalCount
, si le showCount
affiche false, cela signifie que les mises à jour de count
n’entraîneront pas de recalcul.
Au contraire, si le conditionalCount
est lu et que le showCount
affiche true, la fonction de dérivation s’exécutera de manière autonome en envoyant un message qui indique la valeur de count
. Il faut noter que les dépendances peuvent être ajoutées ou supprimées. Si showCount
est défini sur false, alors count
ne sera plus considéré comme une dépendance de conditionalCount
.
Lectures des signaux dans onpush : les composants
Lorsqu’un OnPush
composant utilise la valeur d’un signal dans son modèle, Angular tentera de suivre le signal telle une dépendance du composant. Après que le signal ait été actualisé, le framework marquera automatiquement le composant pour faire en sorte que dans la prochaine détection de changement le composant se met à jour.
Les effets
Un effet est une opération qui s’exécute lorsqu’une ou plusieurs valeurs de signal changent. Il est possible de créer un effet via la fonction effect
:
effect(() => {
console.log(`The current count is: ${count()}`);
});
Les effets vont toujours s’exécuter au moins une fois. Lorsqu’un effet s’exécute, il va suivre toute la valeur de signal lue. Chaque fois qu’une des valeurs de signal change, l’effet s’exécute en amont. De plus, les effets gardent une trace de leurs dépendances de façon dynamique et ils ne suivent que les signaux lus pendant l’exécution. Ces effets s’exécutent toujours de manière asynchrone lors du processus de détection des modifications.
Utilisation des effets
Les effets sont utiles dans des conditions spécifiques. En effet, il existe des situations dans lesquelles la fonction effect sera sollicité :
- Ajout d’un comportement DOM personnalisé ne pouvant pas être exprimé avec la syntaxe du modèle
- Enregistrement des données affichées et de leur modification
- Exécution d’un rendu personnalisé dans une bibliothèque de graphiques ou une autre bibliothèque d’interface utilisateur
- Synchronisation des données avec
window.localstorage
Par contre, il existe également des cas où la fonction effect
est à éviter. Il faut éviter de l’utiliser pour la propagation des changements d’état car cela pourrait entraîner des erreurs, des mises à jour circulaires infinies, ou bien des cycles de détection de changement inutile.
Contexte d’injection
Par défaut, l’enregistrement d’un nouvel effet requiert un contexte d’injection. Pour cela, il est nécessaire d’utiliser la fonction effect
dans un composant, un service constructor
ou une directive :
@Component({...})
export class EffectiveCounterCmp {
readonly count = signal(0);
constructor() {
// Register a new effect.
effect(() => {
console.log(`The count is: ${this.count()})`);
});
}
}
Par ailleurs, l’effet peut être affecté à un autre champ :
@Component({...})
export class EffectiveCounterCmp {
readonly count = signal(0);
private loggingEffect = effect(() => {
console.log(`The count is: ${this.count()})`);
});
}
Afin de créer un effet hors du constructor
, il est possible de passer un Injector
à effect
par ces options :
@Component({...})
export class EffectiveCounterCmp {
readonly count = signal(0);
constructor(private injector: Injector) {}
initializeLogging(): void {
effect(() => {
console.log(`The count is: ${this.count()})`);
}, {injector: this.injector});
}
}
Les effets destructeurs
Un effet est automatiquement détruit lorsque son contexte d’injection est détruit. Tous les effets créés dans les composants vont être détruits, il en va de même pour les effets au sein des directives ou des services. Par ailleurs, les effets renvoient un EffectRef
utile pour détruire manuellement les effets à l’aide de l’opération .destroy()
. Cette opération peut être combinée avec l’option manualCleanup
qui crée un effet qui va durer jusqu’à sa destruction manuelle. Il est important de détruire les effets qui ne sont plus utiles.
Fonctions avancées des signaux
Fonctions d’égalité des signaux
Pendant la création d’un signal, il est possible de fournir une fonction d’égalité qui vérifiera si la nouvelle valeur est différente de la précédente.
import _ from 'lodash';
const data = signal(['test'], {equal: _.isEqual});
// Even though this is a different array instance, the deep equality
// function will consider the values to be equal, and the signal won't
// trigger any updates.
data.set(['test']);
Ces fonctions d’égalité peuvent être utilisées pour les signaux calculés et les signaux inscriptibles. Cependant, concernant les signaux inscriptibles, .mutate()
ne vérifie pas l’égalité, car il modifie la valeur actuelle sans produire de nouvelles références.
Lecture sans suivi de dépendances
Il n’est pas toujours nécessaire d’exécuter du code qui peut lire des signaux dans une fonction réactive comme computed
ou effect
sans créer de dépendance. Par exemple, si en cas de modification de currentUser
, la valeur de a counter
soit enregistrée, cela va créer un effet qui lira deux signaux :
effect(() => {
console.log(`User set to `${currentUser()}` and the counter is ${counter()}`);
});
Dans cet exemple, un message est enregistré lorsque currentUser
ou count
change. Si l’effet ne doit être exécuté que lorsque currentUser
change, alors la lecture de count
n’est qu’accessoire et les changements de count
ne doivent pas enregistrer un nouveau message.
La fonction untracked
est utile lorsqu’un effet doit invoquer un code externe qui ne doit pas être considéré comme une dépendance :
effect(() => {
const user = currentUser();
untracked(() => {
// If the `loggingService` reads signals, they won't be counted as
// dependencies of this effect.
this.loggingService.log(`User set to ${user}`);
});
});
Fonctions de nettoyage des effets
Les effets peuvent lancer des opérations de longue durée, qui seront annulées si l’effet est détruit ou s’il s’exécute de nouveau avant la fin de la première opération. Lors de la création d’un effet, la fonction peut éventuellement accepter une fonction onCleanup
comme premier paramètre. Cette fonction onCleanup
permet d’enregistrer un rappel qui est déclenché avant le début de la prochaine exécution de l’effet ou lors de la destruction de l’effet.
effect((onCleanup) => {
const user = currentUser();
const timer = setTimeout(() => {
console.log(`1 second ago, the user became ${user}`);
}, 1000);
onCleanup(() => {
clearTimeout(timer);
});
});
Vous savez désormais tout concernant les signaux d’Angular, d’autres améliorations et modifications seront très certainement apportées lors de la sortie des nouvelles versions d’Angular.