Aller au contenu

Angular 19 : Tout ce qu’il faut savoir sur les innovations majeures du framework

🚀 Angular 19 est là ! Nouveautés au menu : composants standalone par défaut, linkedSignal pour gérer les états complexes, hydratation incrémentale pour booster les perfs SSR, et bien plus. Découvrez tout ce que cette version peut apporter à vos projets dans cet article.

Nouveautés d'Angular 19

Introduction

Angular est un framework open-source pour créer des applications web modernes et performantes, avec une gestion efficace des données et une expérience utilisateur fluide.

🚀 Depuis le 20 novembre 2024, Angular 19 est enfin disponible apportant une multitude de nouvelles fonctionnalités et d'améliorations pour optimiser le développement et les performances de vos applications. Découvrez linkedSignal, resource, l'hydratation incrémentale et bien plus encore. Plongez dans cet aperçu complet pour découvrir les toutes dernières mises à jour et apprendre comment elles peuvent propulser vos projets à un niveau supérieur ! 

Standalone par défaut

Avec la sortie d'Angular 19, une nouvelle fonctionnalité majeure a été introduite : les composants en standalone par défaut. Cette notion de standalone, introduite dans la version 14 et stable dans la version 15, vise à simplifier la gestion des dépendances et à améliorer la modularité des applications Angular. Un composant standalone est par définition un composant qui ne dépend pas d'un module Angular pour fonctionner. Il peut être utilisé directement sans avoir besoin de l'importer dans un module.

Avec ceci est introduite l'option de configuration strictStandalone à placer dans angularCompilerOptions. Par défaut strictStandalone est à false.

  "angularCompilerOptions": {
    "strictStandalone": true,
  }

Une erreur est générée à la compilation lorsqu'un composant, une directive ou un pipe n'est pas défini en standalone.

Imports inutilisés dans les composants standalone

Marre des imports inutilisés qui encombrent votre code ? Bonne nouvelle ! Angular 19 introduit une fonctionnalité pour signaler ces imports superflus dans les composants standalone :

Si, pour une raison ou une autre, vous préférez désactiver cette vérification, c'est tout à fait possible. Il vous suffit de mettre à jour votre fichier angular.json , ou tsconfig.json, comme suit :

  "extendedDiagnostics": {
    "checks": {
      "unusedStandaloneImports": "suppress"
    }
  }

Variables locales dans les templates

Introduite en developer preview avec Angular v18.1, la syntaxe @let a été largement adoptée par la communauté. Angular passe désormais cette fonctionnalité en stable. Pour en savoir plus sur cette fonctionnalité et ses cas d'usage, consultez l'article détaillé de Nicolas :

Angular 18.1 : découvrez la puissance de @let pour simplifier vos templates
Avec la version 18.1 d’Angular, une nouvelle fonctionnalité révolutionnaire fait son apparition : @let. Découvrez comment l’utiliser et ses divers cas d’usage.

Découvrez le linkedSignal

Le linkedSignal, en phase expérimentale, est une nouvelle fonctionnalité qui permet de créer des signaux réactifs liés entre eux. Un signal, dans ce contexte, est une source de données réactive qui peut être observée par d'autres parties de l'application. Lorsqu'un signal change, toutes les parties de l'application qui dépendent de ce signal sont automatiquement mises à jour. Le linkedSignal permet de lier plusieurs signaux ensemble, facilitant ainsi la gestion des états complexes et des dépendances entre les composants. De plus, unlinkedSignal peut être défini de manière concise. Par exemple :

  multiplier = signal<number>(1);

  result = linkedSignal<number>(() => this.multiplier() * 5);
  console.log(this.result()); // Affiche 5 (1 * 5)

  this.result.set(999);
  console.log(this.result()); // Affiche 999

  this.multiplier.set(2);
  console.log(this.result()); // Affiche 10 (2 * 5)

Avant l'introduction de linkedSignal, Angular proposait déjà des fonctionnalités réactives comme computed. Elles recalculent leur valeur en fonction des autres signaux auxquels elles sont liées. Cependant, les computed sont des signaux en lecture seule, ce qui signifie que leur valeur ne peut pas être directement modifiée par l'utilisateur.

En revanche, le linkedSignal est un WritableSignal, ce qui signifie qu'il peut être à la fois lu et écrit. Cela offre une plus grande flexibilité, car les développeurs peuvent non seulement observer les changements de valeur, mais aussi les déclencher directement. Cette caractéristique rend le linkedSignal particulièrement utile pour les scénarios où les états doivent être à la fois réactifs et modifiables.

Ci-dessous un exemple d'utilisation :

@Component({
  selector: 'app-fruits',
  imports: [FormsModule, MatFormFieldModule, MatSelectModule, MatButtonModule],
  templateUrl: './fruits.component.html',
  styleUrl: './fruits.component.scss'
})
export class FruitsComponent {

  availableFruits = signal<Fruit[]>(randomFruitsArray());

  selectedFruit = linkedSignal<Array<Fruit>, number | undefined>({
    source: this.availableFruits,
    computation: (fruits, previous) => {
      return fruits.some(fruit => fruit.id === previous?.value) ? previous?.value : fruits[0].id;
    }
  });

  changeAvailableFruits() {
    this.availableFruits.set(randomFruitsArray());
  }
}

Comme on peut le voir ici, on dispose d'un premier signal availableFruits contenant un tableau de fruits aléatoires. Le linkedSignal selectedFruit est configuré avec deux paramètres principaux :

  • source : La source de données réactive, ici availableFruits.
  • computation : Une fonction qui détermine la valeur du signal en fonction de la source et de la valeur précédente. Dans ce cas, la fonction vérifie si l'ID du fruit sélectionné précédemment est toujours présent dans le nouveau tableau de fruits. Si oui, il conserve cet ID, sinon il sélectionne l'ID du premier fruit dans le nouveau tableau.

Ainsi on a toujours la main sur la valeur de selectedFruit si on souhaite la mettre à jour manuellement, contrairement au computed.

Hydratation incrémentale

L'hydratation incrémentale est une technique qui permet de rendre progressivement interactif le contenu d'une application Angular rendue côté serveur (SSR). Traditionnellement, lorsque vous utilisez le SSR, l'application Angular génère une version statique de votre page HTML sur le serveur et l'envoie au client. Ensuite, Angular "hydrate" cette page en ajoutant les comportements interactifs nécessaires, ce qui peut être coûteux en termes de performance pour les grandes applications.

Avec l'hydratation incrémentale, Angular 19 permet d'hydrater seulement les parties de la page qui sont visibles ou nécessaires à l'interaction immédiate, plutôt que d'hydrater toute la page en une seule fois. Cela permet de réduire le temps de chargement initial et d'améliorer la réactivité de l'application.

Pour activer l'hydratation incrémentale il suffit d'ajouter la fonction withIncrementalHydration() au provider provideClientHydration().

import { ApplicationConfig } from '@angular/core';
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withIncrementalHydration())
  ]
};

Cette technique repose sur l'utilisation de l'annotation @defer et d'un nouveau déclencheur hydrate. Ce dernier indique à Angular de charger à la volée le code nécessaire ainsi que ses dépendances pour "hydrater" le composant. Une fois hydraté, Angular rejoue les événements qui se sont produits pendant que le composant était "déshydraté", donnant ainsi l'impression que le composant était déjà interactif depuis le début. Cette méthode utilise la librairie d'envoi d'événements développée par Google Search (Wiz). Nous l'aborderons dans la section suivante. Les déclencheurs d'hydratation sont les mêmes que les conditions @defer, c'est-à-dire :

Trigger Description
hydrate on hover Se déclenche lorsque la souris survole une zone spécifiée
hydrate on timer Déclenche après une durée donnée
hydrate on interaction Déclenche lorsque l'utilisateur interagit avec l'élément
hydrate on immediate Se déclenche immédiatement après la fin du rendu du contenu non différé
hydrate on idle Se déclenche lorsque le navigateur a atteint un état inactif
hydrate on viewport Se déclenche lorsque le contenu spécifié entre dans la fenêtre d'affichage
hydrate when {condition} Déclenche lorsque l'expression conditionnelle devient vraie
hydrate never Spécifique que le contenu du bloc doit rester "déshydraté", devenant ainsi complètement statique

Event replay

La fonctionnalité Event Replay, introduite avec Angular 18, est désormais pleinement stable. Lors de la création d'une nouvelle application SSR, la CLI intègre automatiquement l’appel à withEventReplay() requis pour activer cette fonctionnalité.

Grâce à cette fonctionnalité, les événements utilisateur déclenchés pendant la phase d’hydratation sont capturés et rejoués une fois que l’application est totalement chargée. Cela garantit une continuité des interactions utilisateur, éliminant le risque de perte d’actions durant la transition entre le rendu côté serveur et l’interactivité complète côté client.

Routing et modes de rendu

Le rendu hybride, une nouveauté majeure d'Angular 19, offre la possibilité de configurer des stratégies de rendu personnalisées pour chaque route lorsque le rendu côté serveur est activé. Cette fonctionnalité vous permet d'ajuster le comportement de rendu de vos pages en fonction de leurs besoins spécifiques, améliorant ainsi les performances et l'expérience utilisateur. Les modes de rendu disponibles sont les suivants :

  • RenderMode.Server : Génère la page côté serveur à chaque requête pour une sécurité optimale
  • RenderMode.Client : Génère la page côté client pour une meilleure expérience utilisateur
  • RenderMode.Prerender : Génère la page lors du build

Voici un exemple qui utilise tous les modes de rendu :

export const serverRoutes: ServerRoute[] = [
  {
    path: '/fruits/details/:id',
    renderMode: RenderMode.Server
  },
  {
    path: '/dashboard',
    renderMode: RenderMode.Client
  },
  {
    path: '/fruits/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const fruitService = inject(FruitService);
      const fruitIds = await fruitService.getFruitIds();
      return fruitIds.map(id => ({ id }));
    }
  },
  {
    path: '**',
    renderMode: RenderMode.Prerender
  }
];

Pour les routes configurées avec RenderMode.Prerender, il est possible d'utiliser une fonction appelée getPrerenderParams pour définir les paramètres à pré-rendre. Cette fonction retourne une promesse qui fournit un tableau d'objets, où chaque objet spécifie un nom de paramètre et sa valeur. Par exemple, pour une route définie comme /fruits/:id, la fonction getPrerenderParams pourrait renvoyer un tableau tel que [{id: 1}, {id: 2}, {id: 3}]. Cela permettrait à Angular de générer les pages statiques correspondantes pour les chemins /fruits/1, /fruits/2 et /fruits/3.

Resource

Angular introduit une nouvelle API en phase expérimentale, resource, pour nous permettre de récupérer des données à partir d'une API, de connaître l'état de la demande et de mettre à jour les données localement si nécessaire. Voyons un exemple simple de son utilisation :

import { Component, effect, resource } from '@angular/core';

@Component({ ... })
export class ResourceComponent {

  todos = resource<Todo[], unknown>({
    loader: async () => {
      const todos = await fetch(`${TODO_API_URL}`);

      return todos.json();
    }
  });

  constructor() {
    effect(() => {
      console.log("Value: ", this.todos.value());
      console.log("Is loading: ", this.todos.isLoading());
      console.log("Status: ", this.todos.status());
      console.log("Error: ", this.todos.error());
    })
  }
}

Elle nous renvoie un WritableResource contenant les signaux suivants :

  • isLoading : indique si la ressource est en cours de chargement ;
  • value : contient le résultat de la promise ;
  • error : contient l’erreur si la promise est rejetée ;
  • status : contient l'état de la ressource.

Côté template cela donnerait ceci :

<mat-card class="resource-discover">
  <mat-card-header>
    <mat-card-title><h2>Resource discover - {{ todos.status() }}</h2></mat-card-title>
  </mat-card-header>
  <mat-card-content>
    @if (todos.error()) {
      <div class="error">{{ todos.error() }}</div>
    }
    @if (todos.isLoading()) {
      <mat-progress-bar mode="query"/>
    }
    <mat-list>
      <div mat-subheader>Todos</div>
      @for (todo of todos.value(); track todo.id) {
        <mat-list-item>
          <mat-icon matListItemIcon>{{ todo.completed ? 'task_alt' : 'cancel' }}</mat-icon>
          <div matListItemTitle>{{ todo.title }}</div>
        </mat-list-item>
      }
    </mat-list>
  </mat-card-content>
</mat-card>

Il y a une chose importante à savoir sur l'utilisation de la fonction fetch avec la resource. Il ne considère pas les codes de statut comme 404 ou 500 pour des erreurs, il faut donc throw une erreur manuellement :

  todos = resource<Todo[], unknown>({
    loader: async () => {
      const todos = await fetch(`${TODO_API_URL}/does-not-exist`);
      if (!todos.ok) {
        throw new Error(`Could not fetch : ${todos.url}`);
      }
      return todos.json();
    }
  });

Le signal status dans l'API resource peut correspondre à l'un des états suivants :

  • ResourceStatus.Idle, l’état initial ;
  • ResourceStatus.Error, lorsque la promise est rejetée ;
  • ResourceStatus.Loading, lorsque la promise est en attente ;
  • ResourceStatus.Resolved, lorsque la promise est résolue ;
  • ResourceStatus.Local, lorsque la valeur est définie localement ;
  • ResourceStatus.Reloading, lorsque la ressource est en cours de rechargement.

Concernant le dernier statut, celui-ci est appliqué à l'appel de méthode reload qui va relancer l'appel et mettre à jour les données :

<button mat-stroked-button (click)="todos.reload()">Reload todos</button>

Mais plutôt que de recharger manuellement la resource, cette dernière peut prendre en charge un signal grâce à request. Lorsqu'elle est fournie, la resource se rechargera automatiquement si l'un des signaux utilisés dans la request change. Ici, par exemple, je souhaite que la resource actualise les données à chaque fois que todoQuery change :

  todoQuery = signal('');
  todos = resource<Todo[], { todoQuery: string }>({
    request: () => ({ todoQuery: this.todoQuery() }),
    loader: async ({request}) => {
      const todos = await fetch(`${TODO_API_URL}?title_like=^${request.todoQuery}`);
      if (!todos.ok) {
        throw new Error(`Could not fetch : ${todos.url}`);
      }
      return todos.json();
    }
  });

L'exemple ci-dessus peut encore être amélioré : à chaque caractère tapé dans le champ de recherche, une nouvelle requête est envoyée pour récupérer les tâches correspondantes. Ce qui peut entraîner des appels redondants et inutiles. Il est possible d'annuler la requête précédente si besoin, lorsque la resource est rechargée, en utilisant abortSignal en paramètre du loader :

loader: async ({ request, abortSignal }) => {
      const todos = await fetch(`${TODO_API_URL}?title_like=^${request.todoQuery}`, {
        signal: abortSignal
      });
      ...
    }

Ici nous avons utilisé resource qui se base sur les Promise, mais Angular fournit également un équivalent RxJS appelée rxResource. Dans ce cas, la méthode loader renvoie un Observable, mais toutes les autres propriétés restent des signaux.

  todoQuery = signal('');
  todosRx = rxResource<Todo[], { todoQuery: string }>({
    request: () => ({ todoQuery: this.todoQuery() }),
    loader: ({ request }) => {
      return this._httpClient.get<Todo[]>(`${TODO_API_URL}?title_like=^${request.todoQuery}`);
    }
  });

HMR

Avec Angular v19, le remplacement de module à chaud (HMR) est désormais activé par défaut pour le style et disponible en phase expérimentale pour les templates, offrant une amélioration significative pour les développeurs. Avant cette mise à jour, chaque modification de style nécessitait une reconstruction complète de l'application via Angular CLI, suivie d'un rafraîchissement du navigateur. Ce processus pouvait s'avérer long et fastidieux, surtout pour les projets de grande envergure.

Désormais, HMR compile uniquement le style modifié, envoie le résultat directement au navigateur et met à jour l'application en cours sans rafraîchissement de la page ni perte d'état. Voici un exemple de modification de style pour l'élément <h1>, où nous changeons la taille de la police à 4rem et la couleur à deeppink (parce que le rose, c'est cool). Vous remarquerez qu'il n'y a aucun rafraîchissement de la page lors des modifications !

0:00
/0:19

Une vidéo illustrant la modification des styles de l'élément h2. On y voit le changement de la couleur du texte en rose et l'ajustement de la taille de la police à 4rem. Ces modifications sont instantanément appliquées et ne nécessitent pas de rechargement de la page.

Côté template, il faudra activer le HMR expérimentale avec la commande suivante :

NG_HMR_TEMPLATES=1 ng serve

Les signaux sont désormais stables (ou presque)

La plupart des signaux ne sont plus en developer preview et peuvent être utilisés sans risques. Les fonctions suivantes sont maintenant marquées comme stable :

  • input()
  • output()
  • model()
  • viewChild()
  • viewChildren()
  • contentChild()
  • contentChildren()
  • takeUntilDestroyed()
  • outputFromObservable()
  • outputToObservable()

Migrer vers ces nouvelles API peut représenter une charge de travail conséquente. Pour cela, Angular a mis à disposition les schematics suivants pour effectuer la migration complète :

ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migration

De plus, il est possible d'exécuter toutes ces migrations en une seule fois :

ng generate @angular/core:signals

Juste avant je précisais que "presque" tous les signaux étaient stables, car ce n'est pas le cas pour effect qui a subi quelques changements et est encore en phase de développement. Il existe désormais deux types d'effect qui se distinguent par leur moment d'exécution et leur contexte:

  • Component effects : créés quand effect() est appelé à partir de composants ou directives. Ils s'exécutent en tant qu'événements du cycle de vie du composant pendant le processus de synchronisation (détection des changements) d'Angular.
  • Root effects : créés quand effect() est appelé en dehors d'un composant ou lorsque l'option forceRoot est précisée et ne sont donc pas liés directement au cycle de vie des composants.

Un autre changement notable est que vous pouvez maintenant écrire dans les signaux depuis un effect, sans avoir besoin de spécifier l'option (désormais obsolète) allowSignalWrites à true.

Angular Language Service

En plus des schematics, l'Angular Language Service a été mis à jour pour permettre d'effectuer les migrations depuis l'IDE. Pour rappel, un service linguistique fournit aux éditeurs de code un moyen d'obtenir des complétions, des erreurs, des diagnostics et des conseils fichier par fichier.

0:00
/0:07

Une vidéo un développeur modifie un @Input Angular en le survolant. Le service de langage Angular affiche alors une fenêtre contextuelle proposant de convertir cet input en une version basée sur un signal. Le développeur choisit cette option, et le service met automatiquement à jour l'input pour le rendre compatible avec un signal, tout en ajustant le template en conséquence.

Conclusion

Angular 19 marque une étape importante dans l'évolution de ce framework, en apportant des améliorations qui renforcent sa performance, sa flexibilité et sa convivialité pour les développeurs. Parmi les points forts, on retrouve l'hydratation incrémentale, qui optimise le chargement des composants, et l'API resource(), qui simplifie la gestion des données asynchrones tout en gérant automatiquement les états de chargement et d'erreur.

Angular continue de se positionner comme un outil de choix pour le développement web moderne, et cette version 19 laisse entrevoir un avenir encore plus prometteur avec des innovations qui répondent aux besoins croissants des développeurs. À votre tour de découvrir tout son potentiel dans vos projets !  🚀

Dernier