Depuis la sortie d'Angular 14, il est indéniable que chaque version apporte son lot de nouveautés et Angular 16 ne fera pas exception.
Cela fait un moment que l'on entend parler de l'intégration du pattern Signals dans Angular, mais Angular 16 apporte bien plus que cette nouveauté.
De quoi s'agit-il ? Comment les mettre en œuvre ?
Tout ce que vous voulez savoir est ici !
Signaux
Le modèle Signal est en place depuis le début dans la bibliothèque Solid Js et est basé sur le modèle push/pull.
Comme son nom l'indique, pull vous permet de récupérer la valeur et push vous permet d'en définir une nouvelle ou de la modifier.
Quoi qu'il en soit, un signal a toujours une valeur et la récupération de cette valeur se fait toujours de manière synchrone.
Dans la version 16 d'Angular, l'API de base sera intégrée et permettra d'obtenir plus de performance dans la façon dont Angular gère la détection de changements.
La question est de savoir pourquoi ?
Au démarrage d'une application Angular, le framework surcharge certaines API de bas niveau du navigateur, comme la fonction addEventListner.
Cette surcharge est réalisée à l'aide de la bibliothèque Zone JS. Zone est une bibliothèque qu'Angular utilise pour déclencher une détection de changements en écoutant:
- Les événements DOM tels que le clic, le survol de la souris...
- Requêtes HTTP
- fonctions setTimeout, setInterval
Ainsi, lorsque quelque chose change dans votre application, une détection de changements est effectuée et votre page est rafraîchie.
Comme toute application créée avec un framework de rendu côté client, votre application est matérialisée par un arbre de composants.
Dans Angular, chaque composant est associé à un détecteur de changements. Il devient donc logique que si quelque chose change dans l'un des composants enfants, l'arbre entier soit réévalué sans même prendre en compte les dépendances de chaque composant. C'est ce que nous appelons le dirty checking.
Même si la détection des changements de votre application est en mode OnPush, le cycle de détection de changements parcourera l'arbre entier. Contrairement au mode par défaut, les composants OnPush qui n'ont pas de dépendances de changements ne seront pas réévalués.
Il est donc clair que la détection des changements dans Angular n'est pas optimale, et c'est pour résoudre ce problème que l'intégration de Signal sera utile.
Avec Signal, la granularité du niveau de détection de changements se fait au niveau de la variable signal. Ainsi, une détection de changements sera effectuée au niveau du composant uniquement lorsque le signal change, sans qu'il soit nécessaire de parcourir l'ensemble de l'arbre et sans qu'il soit nécessaire d'avoir recours à Zone JS. À l'avenir, Zone JS pourrait être facultative.
Créer un signal dans Angular est facile, il suffit d'appeler la fonction signal avec une valeur initiale.
const counter = signal(0) // crée un signal avec une valeur initiale de 0 ;
console.log(this.counter()) // affiche 0
La fonction signal renvoie un WritableSignal qui nous permet de modifier ou de donner une nouvelle valeur à notre signal.
/**
La fonction Set permet de donner une nouvelle valeur à un signal. Utile si vous avez besoin de changer la structure de données lorsque la nouvelle valeur ne dépend pas de l'ancienne.
Notifier toutes les dépendances.
**/
set(value : T) : void ;
/**
La fonction de mise à jour permet de mettre à jour la valeur du signal si la mise à jour dépend de la valeur précédente.
En d'autres termes, cette fonction permet de mettre à jour la valeur des signaux de manière immuable.
Notifie toutes les dépendances.
**/
update(updateFn : (value : T) => T) : void ;
/**
La fonction de mutation permet de mettre à jour la valeur d'un signal en le mutant sur place.
En d'autres termes, cette fonction est utile pour effectuer des changements internes à la valeur du signal sans changer son identité interne.
Notifie toutes les dépendances.
**/
mutate(mutatorFn : (value : T) => void) : void ;
/**
Retourne un signal en lecture seule.
**/
asReadonly() : Signal<T> ;
Computed
Les signaux calculés permettent de créer des signaux dérivés à partir d'autres signaux de dépendance.
const person = signal<{ firstname : string ; lastname : string}>({ firstname : 'John', lastname : 'Doe'})
// Mise à jour automatique en cas de changement de person() ;
const presentation = computed(() => ${personne().prénom}${personne().nom}` ;
Un signal calculé n'est réévalué que si l'un des signaux dépendants a changé.
Computed peut remplacer le célèbre Pipe.
Effet
Les effets permettent d'effectuer des opérations d'effets secondaires qui lisent la valeur de zéro ou plusieurs signaux, et sont automatiquement programmés pour être ré-exécutés chaque fois que l'un de ces signaux change.
L'Api est conçue comme suit :
function effect(
effectFn : (onCleanup : (fn : () => void) => void) => void,
options ? CreateEffectOptions
) : EffectRef ;
Un exemple concret peut être le suivant:
query = signal('') ;
users = signal([]) ;
effect(async (onCleanup) => {
const controller = new AbortController() ;
const response = await fetch('/users?query=' + query())
users.set(await response.json()) ;
onCleanup(() => controller.abort())
})
Les signaux sont un système réactif primitif dans Angular. Cela signifie qu'ils peuvent être utilisés dans le composant mais aussi à l'extérieur, par exemple dans les services.
Mappage automatique des paramètres de l'url
Imaginons un routage comme celui-ci :
export const routes : Routes = [
{ path : 'search/:id',
component : SearchComponent,
resolve : { searchDetails : searchResolverFn }
}
]
Avant Angular 16, il était obligatoire d'injecter le service ActivatedRoute pour récupérer le paramètre de l'url mais aussi les paramètres de requête ou les données associées à cette url.
@Component({...})
export class SearchComponent {
readonly #activateRoute = inject(ActivatedRoute) ;
readonly id$ = this.#activatedRoute.paramMap(map(params => params.get('id') ;
readonly data$ = this.#activatedRoute.data.(map(({ searchDetails })) => searchDetails)
}
Avec Angular 16, il ne sera plus nécessaire d'injecter le service ActivatedRoute pour récupérer les différents paramètres de la route car ceux-ci peuvent être directement liés aux inputs des composants.
Pour activer cette fonctionnalité, pour une application qui utilise le système de modules, l'option à activer se trouve dans les options de RouterModule.
RouterModule.forRoot(routes, { bindComponentInputs : true })
Pour une application dite standalone, il s'agit d'une fonction à appeler.
provideRoutes(routes, withComponentInputBinding()) ;
Une fois la fonctionnalité activée, le composant est très simplifié.
@Component({...})
export class SearchComponent {
@Input() id! : string ;
@Input() searchDetails! : SearchDetails
}
Input obligatoire
Une fonctionnalité très attendue par la communauté était la possibilité de rendre certains inputs obligatoires.
Jusqu'à présent, plusieurs workaround ont été utilisés pour y parvenir :
- lever une erreur dans le cycle de vie NgOnInit si la variable n'était pas définie
- modifier le sélecteur de notre composant pour inclure les différents inputs obligatoires.
Ces deux solutions avaient leurs avantages mais aussi leurs inconvénients.
A partir de la version 16, rendre un input obligatoire sera un simple objet de configuration à passer aux métadonnées de l'annotation input.
@Input({ required : true }) name! : string ;
Nouvel injecteur DestroyRef
Angular v16 a introduit un nouveau provider appelé DestroyRef, qui permet d'enregistrer des callbacks de destruction pour un périmètre de cycle de vie spécifique. Cette fonctionnalité est applicable aux composants, directives, pipes, vues embarquées, et instances d'EnvironmentInjector.
L'utilisation est assez simple.
@Component({...})
export class AppComponent {
constructor() {
inject(DestroyRef).onDestroy(() => {
// Ecrivez votre logique de nettoyage
})
}
}
Avec ce nouveau fournisseur, Angular nous permet de partager certaines logiques de nettoyage classiques telles que la désinscription de nos observables.
export function untilDestroyed() {
const replaySubject = new replaySubject(1) ;
inject(DestroyRef).onDestroy(() => {
replaySubject.next(true) ;
replaySubject.complete() ;
}) ;
return <T>() => takeUntil<T>(replaySubject.asObservable()) ;
}
@Component({...})
export class AppComponent {
readonly #untilDestroyed = untilDestroyed() ;
ngOnInit() {
interval(1000)
.pipe(this.#untilDestroyed())
.subscribe(console.log) ;
}
}
Vite comme serveur de développement
Avec l'arrivée d'Angular 14 a été introduite la possibilité d'utiliser un nouveau Bundler Javascript : EsBuild
Ce nouveau Bundler a la capacité d'être très rapide et pourrait réduire le temps de build d'environ 40%. Le principal problème est que cette nouvelle fonctionnalité et ce gain de performance ne pouvaient être utilisés que pour un build et non pendant le développement (dev server).
Dans la version 16 d'Angular, Esbuild pourra également être utilisé pendant le développement grâce à Vite.
Pour activer cette fonctionnalité dans le fichier angular.json mettez à jour le builder comme ceci :
"architect" : {
"build" : {
"builder" : "@angular-devkit/build-angular:browser-esbuild",
"options" : { ... }
Attention cette fonctionnalité est encore expérimentale.
Hydratation non destructive
Angular permet de créer des applications de rendu côté serveur avec l'aide d'Angular Universal.
Le problème était qu'en général on pouvait se retrouver à créer des applications peu efficaces, principalement à cause de l'hydratation.
Jusqu'à présent, l'hydratation était destructive. C'est-à-dire que la page entière était détruite puis entièrement reconstruite et rendue une fois que le navigateur avait récupéré le Javascript et l'avait exécuté.
La bonne nouvelle est que les API ont été réécrites pour permettre une hydratation partielle. C'est à dire qu'une fois que le html est chargé et que le DOM est fait, toute la structure sera parcourue pour attacher les différents écouteurs d'événements et recréer l'état de l'application pour rendre l'application réactive mais sans la rendre une nouvelle fois.
Conclusion
La version 16 d'Angular apporte sans aucun doute de belles nouveautés. Certaines d'entre elles sont encore expérimentales, comme les signaux, ou Vite, qui est utilisé comme serveur de développement.
Quoi qu'il en soit ces nouvelles fonctionnalités vont sans aucun doute changer la façon dont nous codons nos applications Angular en les rendant moins boilerplate, encore plus optimisées et en ouvrant la porte à l'intégration de nouvelles technologies comme vitest ou playwright de façon plus simple.