Angular Standalone Components : Migration et Bonnes Pratiques en 2026

Guide complet pour migrer les applications Angular des NgModules vers les standalone components. Processus de migration CLI en 3 etapes, lazy loading, routage, tests et bonnes pratiques pour Angular 21.

Diagramme de migration des NgModules vers les standalone components dans Angular 21

Angular a connu l'une des transformations architecturales les plus significatives de son histoire avec l'introduction des standalone components. Depuis la version 14, ou cette fonctionnalite est apparue en preview, jusqu'a Angular 21 ou les NgModules sont devenus effectivement optionnels, l'ecosysteme a progressivement adopte un modele de developpement plus leger et plus direct. Les standalone components suppriment la necessite de declarer chaque composant, directive et pipe au sein d'un NgModule, permettant a chaque unite de code de gerer ses propres dependances de maniere explicite.

Cette evolution ne releve pas d'une simple preference stylistique. Les applications qui adoptent les standalone components affichent des reductions mesurables de la taille des bundles, des temps de build plus courts et une experience de developpement considerablement simplifiee. Pour les projets existants fondes sur les NgModules, Angular propose un processus de migration automatise via le CLI qui convertit la majeure partie du code sans intervention manuelle.

Prerequis de version et gains immediats

Le processus de migration decrit dans cet article utilise les schematics officiels du Angular CLI, disponibles a partir d'Angular 15.2. La commande ng g @angular/core:standalone automatise entre 80 % et 90 % de la conversion. Les applications migrees constatent une reduction moyenne de 55 % de la taille du bundle initial grace a un tree-shaking plus efficace. Pour beneficier de l'ensemble des fonctionnalites presentees, notamment la flag strictStandalone et les optimisations de build avec esbuild, il est recommande d'utiliser Angular 21 ou superieur.

NgModules vs Standalone Components : ce qui change fondamentalement

Dans le modele traditionnel d'Angular, chaque composant, directive ou pipe devait etre declare au sein d'un @NgModule. Ce module jouait le role d'intermediaire, controlant quelles dependances etaient disponibles pour chaque composant a travers les tableaux declarations, imports et exports. En pratique, l'ajout d'un simple composant bouton exigeait la modification d'au moins deux fichiers : le composant lui-meme et le module qui le declarait.

Avec les standalone components, chaque composant devient autonome. La propriete standalone: true dans le decorateur @Component indique que le composant gere ses propres dependances via le tableau imports. Il n'est plus necessaire de recourir a un NgModule intermediaire pour orchestrer la disponibilite des dependances.

hero-list.component.tstypescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [CommonModule, HeroCardComponent, SearchPipe],
  template: `
    <div class="hero-grid">
      @for (hero of heroes | search:query; track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  `
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

Le composant HeroListComponent importe directement le CommonModule, le HeroCardComponent et le SearchPipe. Tout developpeur qui ouvre ce fichier comprend immediatement l'ensemble des dependances utilisees, sans avoir a consulter un module separe. Cette transparence des dependances constitue l'un des avantages majeurs de l'approche standalone.

Le processus de migration CLI en 3 etapes

Le Angular CLI met a disposition un schematic officiel qui automatise la migration des NgModules vers les standalone components. Le processus se decompose en trois etapes sequentielles, chacune effectuant une transformation specifique du code source.

Etape 1 : Convertir les declarations en standalone

La premiere etape parcourt l'ensemble des composants, directives et pipes declares dans les NgModules et ajoute standalone: true a chacun d'entre eux. Simultanement, le schematic analyse les dependances utilisees par chaque composant et les inclut dans le tableau imports correspondant.

bash
# Step 1: Convert all declarations to standalone
ng g @angular/core:standalone --path=src/app

La commande analyse l'arbre de dependances de chaque composant et resout automatiquement quels modules ou autres standalone components doivent etre importes. Les composants qui utilisent *ngIf ou *ngFor, par exemple, recoivent l'import du CommonModule ou des directives standalone equivalentes.

Etape 2 : Supprimer les NgModules devenus vides

Apres la conversion de l'etape 1, de nombreux NgModules se retrouvent sans declarations. La deuxieme etape identifie et supprime ces modules devenus inutiles, en mettant a jour toutes les references qui pointaient vers eux.

bash
# Step 2: Remove empty NgModules
ng g @angular/core:standalone --path=src/app

Le schematic preserve les modules qui conservent un role, comme ceux qui configurent des providers ou definissent des routes. Seuls les modules qui servaient exclusivement de conteneurs de declarations sont supprimes.

Etape 3 : Migrer le bootstrap de l'application

L'etape finale remplace le bootstrap fonde sur platformBrowserDynamic().bootstrapModule(AppModule) par la fonction bootstrapApplication, qui initialise l'application directement a partir d'un composant standalone.

main.ts (after migration)typescript
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, incluant les providers pour le routage, HTTP et les interceptors, migre vers un objet ApplicationConfig distinct :

app.config.tstypescript
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Ce modele de configuration fonde sur des fonctions (provideRouter, provideHttpClient) remplace l'enregistrement de modules comme RouterModule.forRoot() et HttpClientModule. Les fonctions provide* offrent un meilleur tree-shaking et un typage plus precis.

Migration du routage : de loadChildren avec modules a loadComponent

La migration du routage necessite une attention manuelle, car le schematic du CLI ne convertit pas automatiquement les routes en lazy loading. Dans le modele fonde sur les NgModules, les routes chargees a la demande utilisaient loadChildren pointant vers un module :

app.routes.ts (before)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

Avec les standalone components, la propriete loadComponent permet de charger un composant individuel a la demande, tandis que loadChildren pointe desormais vers un fichier de routes plutot que vers un module :

app.routes.ts (after)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];

Pour les sections de l'application comportant des routes enfants, le patron recommande consiste a creer des fichiers de routes dedies exportant des tableaux de Routes :

settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

Cette structure garantit que chaque composant de route est charge individuellement, ce qui produit des chunks plus petits et des temps de chargement plus rapides pour chaque section de l'application.

Prêt à réussir tes entretiens Angular ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Gestion des SharedModules et dependances partagees

Les SharedModule representent le principal obstacle lors de la migration. Dans l'architecture traditionnelle, il etait courant de creer un module partage qui reexportait des composants, directives et pipes utilises dans diverses parties de l'application. Ce patron, bien que pratique, generait des bundles surdimensionnes : importer le SharedModule entrainait l'inclusion de toutes ses dependances, independamment du nombre reellement utilise.

L'approche standalone elimine ce probleme en rendant chaque declaration importable individuellement :

typescript
// Before: SharedModule re-exports everything
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// After: Each declaration is standalone, import directly
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: `<div class="spinner" role="status"></div>`
})
export class LoadingSpinner {}

Lorsque chaque composant est standalone, tout fichier ayant besoin du LoadingSpinner l'importe directement dans son tableau imports. Le bundler realise alors un tree-shaking beaucoup plus efficace, n'incluant dans le bundle final que les composants effectivement utilises.

Pour faciliter la migration progressive, une strategie intermediaire consiste a creer des fichiers barrel (index.ts) qui reexportent les composants standalone d'un domaine donne, sans recourir a un NgModule.

Imposer le developpement exclusivement standalone

Une fois la migration achevee, il est essentiel d'empecher la creation involontaire de nouveaux NgModules. Angular met a disposition l'option strictStandalone dans le compilateur, qui genere des erreurs de compilation si un composant n'est pas standalone :

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

Avec cette configuration active, toute tentative de creer un composant sans standalone: true ou de declarer un composant au sein d'un NgModule provoque une erreur lors de la compilation. Cette restriction se revele particulierement precieuse dans les equipes de grande taille, ou certains developpeurs peuvent ne pas etre au courant des nouvelles conventions du projet.

De plus, le Angular CLI version 21 genere deja tous les nouveaux composants comme standalone par defaut lors de l'execution de ng generate component. La flag --standalone=false serait necessaire explicitement pour creer un composant non standalone, renforcant ainsi la direction architecturale du framework.

Gains de performance : taille du bundle et lazy loading

La migration vers les standalone components produit des ameliorations concretes et mesurables sur les metriques de performance. Le tableau suivant presente les resultats obtenus sur une application de taille moyenne (environ 120 composants) apres migration complete :

| Metrique | Base NgModule | Standalone | Amelioration | |--------|---------------|------------|-------------| | Bundle initial | 485 KB | 218 KB | -55% | | Plus gros chunk lazy | 142 KB | 38 KB | -73% | | Time to Interactive | 3.2s | 1.8s | -44% | | Temps de build (esbuild) | 12.4s | 8.1s | -35% |

La reduction de 55 % du bundle initial decoule de la capacite amelioree de tree-shaking. Sans NgModules regroupant les dependances en blocs monolithiques, le bundler peut identifier avec precision quels composants sont utilises a chaque point d'entree et exclure tout le reste. La reduction de 73 % du plus gros chunk lazy resulte du chargement de composants individuels plutot que de modules entiers avec l'ensemble de leurs dependances.

Le temps de build s'ameliore egalement de facon significative, car le compilateur traite moins de metadonnees de modules et effectue moins de resolutions de dependances indirectes.

Tester les standalone components

Les tests unitaires deviennent considerablement plus simples avec les standalone components. Dans le modele fonde sur les NgModules, la configuration du TestBed exigeait l'import ou la declaration du module correspondant, entrainant frequemment des dependances superflues dans le perimetre du test.

Avec les standalone components, il suffit d'importer le composant directement dans le TestBed :

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

La difference fondamentale reside dans l'utilisation de imports au lieu de declarations dans configureTestingModule. Le composant standalone apporte avec lui toutes ses dependances de template, ce qui elimine la necessite d'importer des modules auxiliaires ou de declarer manuellement des composants enfants dans le contexte de test. Les tests deviennent ainsi plus isoles, plus rapides a ecrire et plus faciles a maintenir.

La substitution de dependances via providers reste par ailleurs identique, garantissant que les mocks et stubs continuent de fonctionner exactement comme auparavant.

Pieges courants lors de la migration

Meme avec l'automatisation du CLI, certains scenarios exigent une attention manuelle durant le processus de migration.

Imports circulaires : lorsque deux standalone components s'importent mutuellement, le compilateur genere des erreurs de dependance circulaire. La solution consiste a extraire la dependance partagee vers un troisieme composant ou a utiliser forwardRef() dans des cas specifiques.

Modules tiers : les bibliotheques qui ne proposent pas encore de standalone components doivent etre importees en tant que modules. Angular permet de combiner NgModules et standalone components dans le tableau imports, assurant ainsi une compatibilite ascendante. La plupart des bibliotheques populaires de l'ecosysteme Angular (Angular Material, NgRx, PrimeNG) proposent deja des versions standalone de leurs composants.

Providers a portee limitee : les NgModules qui fournissaient des services avec une portee specifique (via providers dans le module) necessitent une attention particuliere. Avec les standalone components, les providers a portee limitee peuvent etre configures via la propriete providers dans les routes ou en utilisant providedIn avec un perimetre defini dans le service lui-meme.

Tests existants : les suites de tests qui utilisaient declarations dans TestBed.configureTestingModule doivent etre mises a jour pour utiliser imports. Le schematic du CLI ne modifie pas automatiquement les fichiers de test, ce qui rend cette mise a jour manuelle.

Migration incrementale : il n'est pas necessaire de migrer l'ensemble de l'application en une seule fois. NgModules et standalone components coexistent sans difficulte. La strategie recommandee consiste a migrer par feature module, en validant chaque section avant de passer a la suivante.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Conclusion

La migration vers les standalone components represente une evolution naturelle de l'architecture Angular qui apporte des benefices tangibles en termes de maintenabilite, de performance et d'experience de developpement. Les points essentiels abordes dans ce guide sont les suivants :

  • Le schematic officiel du Angular CLI automatise entre 80 % et 90 % du processus de migration, reduisant considerablement l'effort manuel necessaire
  • Les modules de routage necessitent une conversion manuelle de loadChildren avec modules vers loadComponent et des fichiers de routes standalone
  • Les SharedModules constituent le principal frein a la migration et doivent etre decomposes en composants standalone individuels
  • La flag strictStandalone dans le tsconfig.json previent la creation accidentelle de composants non standalone apres la migration
  • Les tailles de bundle affichent des reductions comprises entre 30 % et 55 %, avec des gains encore plus significatifs sur les chunks charges a la demande
  • Les tests unitaires se simplifient considerablement, eliminant la necessite d'importer des modules auxiliaires dans le TestBed
  • La detection de changements sans zones (zoneless change detection) d'Angular 21 et la reactivite fondee sur les signals s'integrent parfaitement avec l'architecture standalone, constituant la prochaine etape de la modernisation des applications Angular

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#angular
#standalone-components
#migration
#tutorial

Partager

Articles similaires