Front-end angularforall.com

- Angular 19 : toutes les nouveautés

Angular Angular 19 Nouveautes Template Cli
Angular 19 : toutes les nouveautés

Explorez toutes les nouveautés Angular 19 : variables @let, route render mode, HMR des styles, signal-based forms, standalone par défaut et améliorations.

Résumé des changements Angular 19

Angular 19 consolide l'écosystème des signaux et améliore drastiquement la Developer Experience. Le focus est sur la flexibilité du rendu, la productivité et la finalisation des APIs expérimentales.

À retenir : Angular 19 = nouvelles syntaxes de template + rendu hybride flexible + HMR natif + linkedSignal()/resource() stables. Idéal pour les apps SSR complexes.
Fonctionnalité Statut Angular 19 Impact
@let en template StableMoins de propriétés de composant
Route render mode StableSSG/SSR/CSR par route
linkedSignal() Stable (était expérimental)Signal dérivé ET modifiable
resource() Stable (était expérimental)HTTP déclaratif avec signaux
Hydration incrémentale Stable@defer + SSR = LCP optimisé
HMR styles StableCSS sans rechargement
Effect cleanup StableNettoyage des effets
Standalone par défaut CLI StableFin de NgModule obligatoire

Variable de template @let

@let permet de déclarer des variables locales directement dans le template HTML, sans avoir à créer des propriétés intermédiaires dans le composant.

Avant (Angular 18 et avant) :

// Composant
get fullName() { return `${this.user?.firstName} ${this.user?.lastName}`; }

// Template
<p>{{ fullName }}</p>

Après (Angular 19 avec @let) :

@let user = currentUser();
@let fullName = user.firstName + ' ' + user.lastName;

@if (user) {
  <h2>{{ fullName }}</h2>
  <p>{{ user.email }}</p>
}
Scope : Une variable @let est scoped au bloc où elle est déclarée. Elle n'est pas accessible en dehors du @if ou @for parent.

Route render mode (SSR/SSG/CSR par route)

Angular 19 permet de définir un mode de rendu différent pour chaque route. Plus besoin de choisir entre "tout SSR" ou "tout CSR".

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: '',
    renderMode: RenderMode.Prerender  // SSG : généré à la build
  },
  {
    path: 'blog',
    renderMode: RenderMode.Prerender  // SSG : liste d'articles statique
  },
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Server     // SSR : rendu à chaque requête (contenu dynamique)
  },
  {
    path: 'dashboard',
    renderMode: RenderMode.Client     // CSR : rendu côté client uniquement (authentifié)
  },
  {
    path: 'profile',
    renderMode: RenderMode.Client     // CSR : données utilisateur personnalisées
  }
];

Configuration dans app.config.server.ts

// app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { provideServerRoutesConfig } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = {
    providers: [
        provideServerRendering(),
        provideServerRoutesConfig(serverRoutes)
    ]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
Mode Génération SEO Cas d'usage
Prerender (SSG)À la build MaximalPages statiques, blog, marketing
Server (SSR)À chaque requête BonDonnées dynamiques, contenu frais
Client (CSR)Dans le navigateur LimitéDashboards, pages authentifiées

linkedSignal() — signal dérivé modifiable

linkedSignal() comble un manque : un signal qui dérive d'une source ET reste modifiable. computed() est en lecture seule. linkedSignal() peut être modifié via .set() tout en se réinitialisant quand la source change.

import { signal, linkedSignal } from '@angular/core';

// Cas d'usage : options de pagination liées au nombre de résultats
const totalResults = signal(0);

// pageSize suit totalResults mais peut être modifié par l'utilisateur
const pageSize = linkedSignal(() => {
    return totalResults() > 100 ? 25 : 10;  // Adapte automatiquement
});

console.log(pageSize());  // 10 (totalResults = 0)

totalResults.set(150);
console.log(pageSize());  // 25 (réinitialisé par la source)

// L'utilisateur peut quand même le modifier
pageSize.set(50);
console.log(pageSize());  // 50

// Mais si totalResults change à nouveau, pageSize se réinitialise
totalResults.set(50);
console.log(pageSize());  // 10 (réinitialisé car source a changé)

linkedSignal() avec computation complète

import { signal, linkedSignal } from '@angular/core';

// Cas avancé : item sélectionné lié à une liste
const items = signal(['Apple', 'Banana', 'Cherry']);
const selectedIndex = signal(0);

const selectedItem = linkedSignal({
    source: () => ({ items: items(), index: selectedIndex() }),
    computation: ({ items, index }) => items[index] ?? null
});

console.log(selectedItem());  // 'Apple'
selectedIndex.set(2);
console.log(selectedItem());  // 'Cherry'

// L'utilisateur peut forcer une valeur différente
selectedItem.set('Override');
console.log(selectedItem());  // 'Override'

// Mais si source change, recalcul automatique
items.set(['X', 'Y', 'Z']);
console.log(selectedItem());  // 'Z' (recalculé)
Quand utiliser linkedSignal() : filtres de formulaire qui ont une valeur par défaut basée sur le contexte, pagination, sélection d'item dans une liste filtrée, valeur par défaut dynamique qui reste overridable.

resource() — chargement async déclaratif

resource() est une API déclarative pour charger des données asynchrones basée sur des signaux. Elle gère automatiquement les états de chargement, d'erreur, et annule les requêtes obsolètes quand les dépendances changent.

import { Component, signal, resource } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { firstValueFrom } from 'rxjs';

interface Article {
    id: number;
    title: string;
    body: string;
}

@Component({
    selector: 'app-articles',
    standalone: true,
    template: `
        <input [value]="searchQuery()"
               (input)="searchQuery.set($event.target.value)"
               placeholder="Rechercher...">

        @if (articles.isLoading()) {
            <p>Chargement...</p>
        } @else if (articles.error()) {
            <p class="text-danger">Erreur : {{ articles.error() }}</p>
        } @else {
            <ul>
                @for (article of articles.value(); track article.id) {
                    <li>{{ article.title }}</li>
                }
            </ul>
        }
    `
})
export class ArticlesComponent {
    private http = inject(HttpClient);

    searchQuery = signal('');

    // resource() recharge automatiquement si searchQuery change
    articles = resource({
        request: () => ({ q: this.searchQuery() }),  // Paramètres réactifs
        loader: async ({ request, abortSignal }) => {
            // abortSignal annule la requête si la source change
            return firstValueFrom(
                this.http.get<Article[]>(`/api/articles?q=${request.q}`)
            );
        }
    });
}

Propriétés disponibles sur resource()

// Les propriétés d'un resource sont des signaux
const articles = resource({ ... });

articles.value()      // Article[] | undefined — données chargées
articles.isLoading()  // boolean — chargement en cours
articles.error()      // unknown — erreur si échouée
articles.status()     // 'idle' | 'loading' | 'resolved' | 'error' | 'reloading'

// Méthodes
articles.reload()     // Forcer un rechargement
articles.set([])      // Écraser la valeur manuellement (local override)
resource() vs toSignal(httpClient.get()) : resource() gère l'annulation automatique des requêtes obsolètes (race condition), les états intermédiaires (isLoading, error), et le rechargement manuel. toSignal() est plus simple mais sans ces features.

Hydration incrémentale avec @defer

L'hydration incrémentale (stable en Angular 19) combine le rendu SSR et la directive @defer : les parties de page sont d'abord rendues en HTML statique côté serveur, puis hydratées progressivement côté client selon des déclencheurs.

// app.config.ts — activer l'hydration incrémentale
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
    providers: [
        provideClientHydration(withIncrementalHydration())
    ]
};
<!-- Utilisation dans les templates -->
<!-- Zone principale : hydratée immédiatement -->
<app-header />
<app-hero />

<!-- Section commentaires : hydratée quand visible -->
@defer (hydrate on viewport) {
    <app-comments />
} @loading {
    <div class="skeleton">Chargement des commentaires...</div>
}

<!-- Sidebar : hydratée au hover -->
@defer (hydrate on hover) {
    <app-sidebar-recommendations />
}

<!-- Zone footer : hydratée quand idle -->
@defer (hydrate on idle) {
    <app-footer-heavy />
}

Déclencheurs d'hydration disponibles

DéclencheurDescriptionCas d'usage
hydrate on viewportVisible dans le viewportContenu below-the-fold
hydrate on interactionClick ou keydownWidgets interactifs
hydrate on hoverSurvol de la sourisMenus, tooltips
hydrate on idleNavigateur inactif (requestIdleCallback)Footer, widgets non critiques
hydrate when conditionSignal/expression truthyLogique conditionnelle
hydrate neverJamais hydraté (HTML statique pur)Contenu entièrement statique
Impact LCP : L'hydration incrémentale améliore significativement le Time to Interactive (TTI) et le LCP en évitant d'hydrater d'un coup toute la page. Le HTML SSR est interactif immédiatement pour les parties critiques.

HMR des styles sans rechargement

Angular 19 améliore le Hot Module Replacement : les modifications de styles CSS/SCSS sont appliquées instantanément sans rechargement de page. L'état de l'application est préservé.

# HMR des styles activé par défaut en ng serve
ng serve

# Pour désactiver
ng serve --no-hmr
  • Styles globaux (styles.scss) : mis à jour sans rechargement
  • Styles de composants : mis à jour sans rechargement
  • Templates HTML : rechargement complet (pas encore HMR)

Effect cleanup et afterRenderEffect

Angular 19 introduit deux améliorations majeures pour la gestion des effets.

1. Effect cleanup : une fonction de nettoyage exécutée avant chaque ré-exécution de l'effet ou à la destruction du composant.

effect((onCleanup) => {
  const ws = new WebSocket(this.url());

  onCleanup(() => ws.close()); // appelé avant la prochaine exécution
});

2. afterRenderEffect() : un effet qui s'exécute après chaque rendu, uniquement côté client. Parfait pour les mesures DOM ou les animations.

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

export class ChartComponent {
  constructor() {
    afterRenderEffect(() => {
      // Accès sûr au DOM après le rendu
      this.drawChart();
    });
  }
}

Standalone par défaut dans le CLI

À partir d'Angular 19, tous les schematics CLI génèrent du code standalone par défaut.

# Nouveau projet : pas de AppModule
ng new my-app

# Composant standalone automatiquement
ng generate component header
# → standalone: true sans option supplémentaire

# Directive standalone
ng generate directive highlight

# Pipe standalone
ng generate pipe truncate
Rétrocompatibilité : Les projets existants avec NgModules continuent de fonctionner. Ajoutez --no-standalone pour générer en mode classique.

Migration d'Angular 18 à 19

La migration est automatisée et sans breaking changes majeurs.

1. Mettre à jour les dépendances :

ng update @angular/cli@19 @angular/core@19

2. Vérifier avant de migrer :

ng update @angular/core@19 --dry-run

Prérequis :

  • TypeScript 5.5 ou supérieur
  • Node.js 18.19+ ou 20.9+
  • RxJS 7.4+ (inchangé)
Migration progressive : @let, le route render mode et l'HMR sont optionnels. Vous pouvez les adopter progressivement sans toucher au code existant.

Partager