Standalone Components Angular : comprendre la nouvelle architecture par défaut

Partager
Standalone Components Angular : comprendre la nouvelle architecture par défaut

Auteur : Etienne Perianayagassamy


Angular a connu une évolution majeure ces dernières années. Parmi les transformations les plus profondes de son écosystème, l'arrivée des Standalone Components a redéfini la manière dont nous structurons et développons nos applications.

Pendant longtemps, le passage obligatoire par les @NgModule représentait une barrière à l'entrée et une source de complexité inutile. Cet article explore les raisons de ce changement, le fonctionnement des Standalone Components, leurs avantages et inconvénients, ainsi que la raison pour laquelle ils constituent aujourd'hui l'approche privilégiée pour le développement de nouvelles applications Angular.


1. Avant les Standalone Components

Avant l'arrivée des Standalone Components, les composants, directives et pipes devaient être déclarés dans un @NgModule pour pouvoir participer au système de compilation d'Angular et être utilisés dans l'application.

Le rôle historique de @NgModule

Un module Angular avait pour objectif de :

  1. Délimiter le périmètre de compilation (savoir quels composants ont accès à quelles directives/pipes).
  2. Organiser le code en regroupant logiquement des fonctionnalités (Feature Modules).
  3. Déclarer des providers et contribuer à la configuration de l'injection de dépendances, même si celle-ci ne reposait pas exclusivement sur les NgModule.

Les limites et la complexité induite

Si ce système offrait une structuration rigoureuse adaptée aux très grands projets d'entreprise, il est rapidement devenu une source de lourdeur architecturale :

  • Verbosité et boilerplate : pour utiliser un simple composant ButtonComponent dans un autre composant UserCardComponent, il fallait s'assurer qu'ils appartenaient au même module, ou bien exporter le bouton depuis son module d'origine et importer ce module dans celui du composant cible.
  • Le piège du SharedModule : pour éviter de multiplier les petits modules, il était courant de créer un immense SharedModule regroupant de nombreux composants, directives et pipes. Cette approche pouvait rendre les dépendances moins explicites, réduire l'isolation entre les fonctionnalités et compliquer l'évolution de l'application.
  • Courbe d'apprentissage raide : pour les développeurs débutants ou venant d'autres frameworks (comme React ou Vue), comprendre la distinction entre le module ES6 (import ... from ...) et le module Angular (@NgModule({ imports: [...] })) était une source de confusion majeure.
  • Tests unitaires alourdis : tester un composant nécessitait souvent de configurer un TestBed avec l'ensemble du module qui le déclarait (ou un module de test dédié), même pour des tests simples et isolés.

2. Qu'est-ce qu'un Standalone Component et quand sont-ils apparus ?

Un Standalone Component est un composant Angular qui peut être utilisé sans être déclaré dans un @NgModule. Il déclare directement les dépendances nécessaires à son template grâce à son tableau imports.

Chronologie d'apparition et d'adoption

  • Angular 14 (mai 2022) : introduction en version Developer Preview. Les développeurs ont pu expérimenter la fonctionnalité sur des projets pilotes.
  • Angular 15 (novembre 2022) : passage en version Stable. La fonctionnalité est officiellement prête pour la production.
  • Angular 17 (novembre 2023) : la CLI génère désormais les nouveaux projets (ng new) et les nouveaux composants (ng generate component) en mode standalone par défaut.
  • Angular 19 (novembre 2024) : bascule du défaut au niveau du compilateur lui-même — standalone: true n'a plus besoin d'être écrit explicitement dans le décorateur @Component (ni @Directive, ni @Pipe), c'est désormais la valeur implicite.

Ces deux derniers jalons sont importants à distinguer : le premier concerne l'outillage (ce que la CLI génère), le second concerne le comportement du compilateur lui-même.

Anatomie d'un Standalone Component

Dans les versions actuelles d'Angular, un nouveau composant est standalone par défaut, sans qu'il soit nécessaire de préciser la propriété standalone: true (celle-ci reste toutefois utile à connaître pour lire du code écrit avec des versions antérieures à Angular 19). Le composant déclare directement les dépendances nécessaires à son template via le tableau imports.

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-user-profile',
  imports: [CommonModule, MatButtonModule],
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
  // Logique du composant
}

Note : la notion de standalone s'applique également aux Directives et aux Pipes.

Le tableau imports déclare explicitement les composants, directives, pipes ou NgModule dont le template a besoin.

Un composant standalone peut importer directement d'autres composants, directives et pipes standalone, mais également des NgModule existants. Cette interopérabilité facilite notamment la migration progressive d'une application historique.


3. Bénéfices et inconvénients des Standalone Components

Les bénéfices

  1. Simplicité et lisibilité : suppression de la « taxe @NgModule ». La structure du projet devient plus plate, plus lisible et plus intuitive.
  2. Architecture orientée composants : chaque composant constitue une unité plus indépendante et explicite dans l'architecture de l'application, facilitant la réutilisabilité, le refactoring et les tests unitaires.
  3. Tests unitaires simplifiés : un composant standalone peut être importé directement dans un TestBed, sans configurer de module de test dédié. Cela n'élimine pas le besoin de fournir des providers, des mocks ou d'autres configurations spécifiques au test, mais cela retire l'étape intermédiaire de création d'un module :
TestBed.configureTestingModule({
  imports: [UserProfileComponent] // import direct, pas de module intermédiaire
});
  1. Lazy-loading granulaire et simplifié : avec l'approche basée sur les modules, le lazy loading reposait généralement sur loadChildren et un NgModule dédié. Avec les Standalone Components, le routeur propose loadComponent, permettant de charger directement et de manière différée un composant sans créer de module dédié :
import { Route } from '@angular/router';

export const ROUTES: Route[] = [
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
  }
];
  1. Réduction de la taille des bundles initiaux : deux mécanismes distincts se combinent particulièrement bien avec les Standalone Components. D'une part, le lazy loading par composant (loadComponent) permet de différer le chargement d'un composant et de ses dépendances jusqu'au moment où l'utilisateur en a réellement besoin, réduisant ainsi la quantité de code chargée initialement. D'autre part, la granularité de l'architecture standalone peut faciliter une gestion plus fine du graphe de dépendances, que les outils de build peuvent ensuite optimiser en supprimant le code inutilisé. L'ampleur du gain dépend fortement du projet, notamment de son architecture et de la granularité du découpage des routes ; il est donc préférable de le mesurer sur son propre cas plutôt que de se fier à un pourcentage générique.

Les inconvénients et points d'attention

  1. Répétition des imports : si plusieurs composants utilisent les mêmes dépendances de base, comme CommonModule ou ReactiveFormsModule, celles-ci doivent être déclarées dans le tableau imports de chaque composant concerné. Ce point est notamment atténué depuis Angular 17 grâce au nouveau Control Flow, qui ne nécessite pas l'import des directives historiques NgIf, NgFor et NgSwitch.
  2. Gestion de l'existant (projets legacy) : la migration vers une architecture standalone peut être progressive. Les composants standalone peuvent notamment importer des NgModule existants, ce qui permet de faire cohabiter les deux architectures pendant une période de transition.
  3. Discipline à maintenir sur un projet en migration : rien n'empêche techniquement de recréer un composant, une directive ou un pipe non-standalone au milieu d'un projet en cours de migration. Angular propose l'option de compilation strictStandalone, définie dans les angularCompilerOptions du tsconfig.json. Lorsqu'elle est activée, tout nouveau composant, directive ou pipe doit être standalone, sous peine d'entraîner une erreur de compilation. Cette option ne supprime pas la possibilité d'utiliser des NgModule, notamment pour certaines configurations existantes. En revanche, elle permet d'éviter l'introduction de nouvelles déclarations non-standalone et peut ainsi sécuriser une migration progressive.

Pour certains cas spécifiques liés à l'injection de dépendances, Angular fournit également des mécanismes comme importProvidersFrom() afin de récupérer les providers issus d'une architecture basée sur les modules.


4. Pourquoi l'approche Standalone est-elle devenue la norme ?

Les Standalone Components sont aujourd'hui l'approche privilégiée pour le développement de nouvelles applications Angular et constituent un élément central de l'évolution du framework.

L'architecture par défaut des nouvelles applications

Comme évoqué précédemment, depuis Angular 17, la CLI privilégie l'architecture standalone lors de la création d'une nouvelle application : ng new ne génère plus de fichier AppModule, et ng generate component produit directement des composants standalone.

Bootstrapping simplifié

Dans une application basée sur les Standalone Components, l'initialisation ne nécessite plus de passer par platformBrowserDynamic().bootstrapModule(AppModule). Elle peut s'appuyer directement sur l'API bootstrapApplication() :

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

La configuration globale de l'application est regroupée dans un objet de configuration, appConfig, construit à partir de fonctions provide* puis transmis à bootstrapApplication(). Le routeur, par exemple, se configure ainsi sans passer par RouterModule.forRoot() :

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { ROUTES } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(ROUTES),
    // provideHttpClient(), provideAnimations(), etc.
  ]
};

Synergie avec les nouvelles fonctionnalités

L'approche Standalone s'inscrit naturellement dans les évolutions récentes d'Angular :

  • Control Flow (@if, @for, @switch) : permet d'utiliser les nouvelles structures de contrôle sans importer les directives historiques NgIf, NgFor et NgSwitch, réduisant ainsi certains besoins liés à CommonModule.
  • Deferrable Views (@defer) : permettent de différer le chargement et le rendu de certaines portions de l'interface. Les dépendances éligibles peuvent alors être chargées de manière différée, ce qui s'intègre naturellement à l'architecture basée sur les composants standalone.
  • Signals : bien qu'indépendants des Standalone Components, ils participent également à l'évolution vers une architecture Angular plus moderne, plus locale et davantage centrée sur les composants.

Conclusion

L'arrivée des Standalone Components marque la fin d'une époque pour Angular : celle d'un framework puissant mais souvent jugé trop rigide et verbeux. En réduisant considérablement la nécessité d'utiliser les @NgModule pour structurer une application, Angular a gagné en simplicité et en lisibilité, tout en conservant la compatibilité avec les architectures existantes.

La CLI génère des composants standalone par défaut depuis Angular 17 et, depuis Angular 19, les composants sont standalone par défaut au niveau du compilateur.

Pour les nouveaux projets, l'approche Standalone est aujourd'hui la solution privilégiée dans l'écosystème Angular. Pour les applications existantes, sa migration peut être adoptée progressivement, éventuellement sécurisée par l'option strictStandalone, en fonction du contexte et des besoins du projet.

Lire la suite