Angular Standalone Components : Migration et Bonnes Pratiques pour Angular 22

Guide complet pour migrer les applications Angular des NgModules vers les standalone components. Le schematic CLI officiel en 3 étapes, les composants sans sélecteur, OnPush par défaut, et le lazy loading avec loadComponent dans Angular 22.

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

Angular a instauré les standalone components comme paradigme par défaut depuis Angular 19, et Angular 22 introduit deux changements majeurs : la stratégie OnPush par défaut et les composants sans sélecteur (selectorless) en version stable. La migration depuis les NgModules s'effectue via un schematic CLI officiel en trois passes, automatisant la majeure partie du processus.

Point clé

Le schematic officiel du Angular CLI gère automatiquement la migration en trois passes. Une application d'entreprise peut terminer la conversion en un seul sprint, avec des réductions de taille de bundle de 30 à 50 % grâce au lazy loading par composant.

NgModules vs Standalone Components : ce qui a changé

Les NgModules servaient de contexte de compilation pour les composants depuis Angular 2. Chaque composant, directive et pipe devait être déclaré dans exactement un module, et les fonctionnalités partagées nécessitaient des importations et exportations de modules soigneusement orchestrées. Cette approche créait un couplage étroit entre fonctionnalités non liées et rendait le tree-shaking difficile.

Les standalone components inversent ce modèle. Chaque composant déclare ses propres dépendances directement dans le tableau imports du décorateur @Component. Plus de registration de module, plus de SharedModule, plus de barrel exports de la moitié de l'application. À partir d'Angular 22, ce pattern est simplifié avec les composants sans sélecteur, permettant des imports directs sans sélecteurs textuels.

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

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [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 tableau imports remplace l'ensemble du graphe de dépendances des NgModules. Le bundler voit exactement quels composants, pipes et directives chaque fichier nécessite, permettant un tree-shaking précis. Le CommonModule n'est plus nécessaire dans Angular 22 car la syntaxe de contrôle de flux (@for, @if) est intégrée au compilateur.

Le processus de migration CLI en 3 étapes

Angular fournit un schematic automatisé qui gère la migration en trois passes séquentielles. Chaque étape s'appuie sur la précédente, et le projet doit compiler correctement entre chaque passe.

Étape 1 : Convertir les déclarations en standalone

La première passe analyse chaque composant, directive et pipe du projet, ajoute standalone: true, et déplace les imports nécessaires depuis leur NgModule parent vers le tableau imports de chaque composant.

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

Sélectionner "Convert all components, directives and pipes to standalone" lorsque l'invite apparaît. Le schematic utilise une analyse statique pour résoudre les dépendances, tout composant dont les métadonnées ne peuvent être analysées au build sera ignoré avec un avertissement.

Étape 2 : Supprimer les NgModules inutiles

Avec toutes les déclarations désormais standalone, de nombreux NgModules deviennent des coquilles vides. Cette passe identifie les modules qui ne faisaient que réexporter des déclarations standalone et les supprime.

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

Sélectionner "Remove unnecessary NgModule classes". Les modules contenant encore des providers, des configurations de routes, ou importés par plusieurs autres modules seront préservés avec un commentaire TODO pour revue manuelle.

Prérequis Angular 22

Angular 22 nécessite TypeScript 6 et Node.js 26. Les versions antérieures ne sont pas supportées. Exécuter ng version pour vérifier la compatibilité avant de commencer la migration.

Étape 3 : Passer au bootstrap standalone

La passe finale remplace le NgModule racine par l'API bootstrapApplication d'Angular et convertit le composant racine en 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));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } 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: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Le pattern ApplicationConfig remplace les tableaux providers et imports du module racine. Toutes les fonctions provider (provideRouter, provideHttpClient, provideAnimations) fonctionnent directement sans wrappers de modules.

Angular 22 : OnPush par défaut et composants sans sélecteur

Angular 22 introduit deux changements qui affectent le développement de composants standalone : OnPush comme stratégie de détection de changements par défaut et les composants sans sélecteur en version stable.

OnPush par défaut

Les nouveaux composants générés dans Angular 22 utilisent ChangeDetectionStrategy.OnPush par défaut. Cela encourage la réactivité basée sur les signals plutôt que les vérifications déclenchées par Zone.js. Pendant la migration, le schematic ajoute explicitement changeDetection: ChangeDetectionStrategy.Default aux composants existants pour maintenir la compatibilité ascendante.

typescript
// Angular 22: OnPush is implicit for new components
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush is now the default
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

Pour les applications migrées, l'adoption progressive d'OnPush passe par le remplacement des propriétés de classe par des signals. Cette approche s'aligne avec l'architecture signal-first promue par Angular 22.

Composants sans sélecteur (stable)

Les composants sans sélecteur permettent d'importer des composants directement dans les templates sans définir de sélecteurs textuels. Cela élimine les conflits de noms de sélecteurs dans les grandes bases de code et offre une meilleure sécurité de typage.

typescript
// Traditional approach with selector
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// Selectorless approach (Angular 22+)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

Dans les templates, les composants sans sélecteur sont utilisés par leur nom de classe plutôt que par une balise de sélecteur personnalisée. Le compilateur s'assure que l'import existe, détectant les erreurs au build plutôt qu'au runtime.

Migration du routage : des modules à loadComponent

Les modules de routage nécessitent une attention manuelle car le schematic ne convertit pas automatiquement les imports de modules loadChildren en loadComponent ou loadChildren au niveau des routes avec des routes standalone.

Le pattern legacy chargeait des modules de fonctionnalités entiers :

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

L'équivalent standalone charge des composants individuels ou des fichiers de routes directement :

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)
  }
];
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)
      }
    ]
  }
];

loadComponent charge un seul composant en lazy loading. loadChildren avec un fichier de routes charge une zone de fonctionnalité entière en lazy loading. Les deux produisent des chunks séparés que le navigateur récupère à la demande.

Prêt à réussir tes entretiens Angular ?

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

Gestion des SharedModules et dépendances communes

Les SharedModules, ces modules fourre-tout qui exportent les composants, directives et pipes couramment utilisés, constituent le blocage le plus fréquent lors de la migration. Le schematic ne peut pas les supprimer automatiquement car plusieurs modules les importent.

La solution : convertir les déclarations partagées en standalone individuellement, puis supprimer le SharedModule une fois que plus rien ne l'importe.

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 {}

Les consommateurs importent désormais LoadingSpinner directement au lieu du SharedModule entier. Le bundler n'inclut que les composants spécifiques dont chaque route a besoin.

Imposer le développement exclusivement standalone

Après la migration, empêcher les nouveaux NgModules de réapparaître dans la base de code est essentiel. Angular fournit une option du compilateur TypeScript pour cela.

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

Avec strictStandalone activé, toute tentative de créer un composant, directive ou pipe non-standalone produit une erreur de compilation. Cela impose la nouvelle architecture à l'ensemble de l'équipe. La migration des formulaires Angular suit le même pattern avec les Signal Forms désormais stables dans Angular 22.

Gains de performance : taille du bundle et lazy loading

Le principal avantage de performance des standalone components provient du lazy loading granulaire. Avec les NgModules, le lazy loading opérait au niveau du module : importer un composant d'un module entraînait toutes les déclarations que ce module exportait. Les standalone components brisent ce couplage.

Un benchmark réel sur une application d'entreprise de taille moyenne (200+ composants) a mesuré avant et après la migration standalone :

MétriqueBase NgModuleStandaloneAmélioration
Bundle initial485 KB218 KB-55%
Plus gros chunk lazy142 KB38 KB-73%
Time to Interactive3.2s1.8s-44%
Temps de build (esbuild)12.4s8.1s-35%

Ces chiffres résultent de la suppression du surcoût de résolution des modules et de la capacité du bundler à éliminer les exports inutilisés au niveau du composant plutôt qu'au niveau du module.

Tester les standalone components avec Vitest

Les tests unitaires se simplifient considérablement avec les standalone components. Angular 22 a fait de Vitest le runner de tests par défaut, remplaçant Karma. La configuration du TestBed ne nécessite plus l'import de modules entiers pour satisfaire les dépendances d'un composant.

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

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);
  });
});

Le composant va directement dans le tableau imports de TestBed.configureTestingModule. Toutes ses dépendances déclarées sont déjà résolues via ses propres imports, aucun import de module supplémentaire n'est nécessaire.

Pièges courants de la migration

Imports circulaires entre standalone components. Quand le composant A importe le composant B et B importe A, le compilateur TypeScript génère une erreur de dépendance circulaire. La solution : extraire l'interface partagée dans un fichier séparé ou utiliser forwardRef() comme contournement temporaire pendant la refactorisation de la chaîne de dépendances.

Bibliothèques tierces utilisant encore des NgModules. De nombreuses bibliothèques ont migré vers standalone, mais certains packages legacy exportent encore des NgModules. Importer ces modules directement dans le tableau imports du composant standalone fonctionne, Angular supporte le mélange d'imports standalone et basés sur les modules.

Providers manquants après suppression de l'AppModule. Les services précédemment fournis dans le tableau providers du module racine doivent migrer vers l'ApplicationConfig dans app.config.ts ou utiliser providedIn: 'root' dans le décorateur @Injectable. Les services à portée de route doivent utiliser le tableau providers dans les configurations de routes.

Question d'entretien

Lors d'une question sur les standalone components en entretien Angular, se concentrer sur trois points : (1) la gestion des dépendances passe du niveau module au niveau composant, (2) le tree-shaking devient granulaire, et (3) la configuration des tests se simplifie car les composants déclarent leurs propres imports.

Sources

Migration vers standalone : points clés pour Angular 22

  • Le schematic officiel du CLI Angular automatise 80-90% de la migration en trois passes séquentielles : convertir les déclarations, supprimer les modules, changer le bootstrap
  • Les modules de routage nécessitent une conversion manuelle de loadChildren avec NgModules vers loadComponent ou des fichiers de routes standalone
  • Les SharedModules sont le principal blocage, convertir chaque déclaration partagée en standalone individuellement, puis supprimer le module
  • Activer strictStandalone dans tsconfig pour empêcher l'introduction de nouveaux NgModules après la migration
  • Les tailles de bundle diminuent de 30-55% grâce au tree-shaking au niveau composant et au lazy loading granulaire avec loadComponent
  • Les tests unitaires se simplifient avec Vitest comme runner par défaut, importer le composant standalone directement dans TestBed sans configuration de module
  • OnPush par défaut et les composants sans sélecteur d'Angular 22 s'associent naturellement avec l'architecture standalone pour une base de code plus propre et plus maintenable

Passe à la pratique !

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

Défi du jour

Tu saurais repérer le bug en Angular ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 21 août 2026

Tags

#angular
#standalone-components
#migration
#tutorial

Partager

Articles similaires