Depuis Angular 19, chaque composant, directive et pipe est standalone par défaut — vous n’écrivez plus standalone: true et vous n’avez plus besoin d’un NgModule. Chaque composant déclare ses propres dépendances dans imports, et l’application démarre avec une fonction, pas avec un module.
Comment l’application démarre
Trois fichiers : main.ts démarre l’application, app.config.ts contient les providers globaux et app.ts est le composant racine. Pour construire une petite application par-dessus, suivez Votre première application, pas à pas.
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));- v21 : les nouvelles applications sont zoneless — pas de
zone.jsdans le bundle, pas de provider spécial. - v21 :
HttpClientest disponible dans root sansprovideHttpClient(). Vous ne l’appelez que pour configurer (interceptors, XHR). - v22 : les composants sont
OnPushpar défaut. Voir Change detection. - v22 : TypeScript 6 est requis.
| Version d’Angular | 22.2.2 |
| Zone.js chargé ? | non — application zoneless |
| Le composant racine est-il standalone ? | oui |
| NgModules écrits dans l’application | 0 |
| Routes configurées (niveau racine) | 4 |
| Pages chargées en lazy (un chunk chacune) | 75 |
imports : chaque composant déclare ses dépendances
Le template d’un composant ne voit que ce qui figure dans ses imports : autres composants, directives, pipes. Les dépendances deviennent explicites, le tree-shaking est meilleur et le lazy loading devient trivial.
import { Component, input } from '@angular/core';
import { CurrencyPipe, DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Avatar } from '@shared/ui/avatar';
@Component({
selector: 'app-user-card',
// Pas de CommonModule : @if/@for font partie du langage, les pipes s’importent un par un
imports: [RouterLink, DatePipe, CurrencyPipe, Avatar],
template: `
<app-avatar [name]="user().name" />
<a [routerLink]="['/users', user().id]">{{ user().name }}</a>
<small>membre depuis {{ user().joinedAt | date: 'MMM yyyy' }}</small>
`,
})
export class UserCard {
readonly user = input.required<User>();
}CommonModule était importé pour *ngIf, *ngFor, ngClass et les pipes. Désormais, le control flow fait partie du langage, [class] remplace ngClass et vous importez les pipes un par un (DatePipe, CurrencyPipe). Résultat : des imports courts et précis.
Avant et maintenant
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { authInterceptor } from './core/auth/auth-interceptor';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(), // erreurs globales → ErrorHandler
provideRouter(routes, withComponentInputBinding()), // routeur + paramètres en input()
provideHttpClient(withInterceptors([authInterceptor])), // seulement si vous voulez des interceptors
// Plus nécessaire : provideZonelessChangeDetection() (par défaut depuis v21)
// Plus nécessaire : un simple provideHttpClient() (HttpClient est dans root depuis v21)
],
};// Avant (Angular ≤ 14) : tout passait par un NgModule
@NgModule({
declarations: [AppComponent, HeaderComponent, UserCardComponent],
imports: [BrowserModule, HttpClientModule, RouterModule.forRoot(routes)],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }],
bootstrap: [AppComponent],
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);Mais j’ai des bibliothèques avec des NgModules…
Pas de problème : un NgModule peut aller directement dans les imports d’un composant standalone. Pour les providers au niveau de l’application issus d’anciens modules, il y a importProvidersFrom().
// Une ancienne bibliothèque qui n’exporte qu’un NgModule ? Importez-le directement.
@Component({
imports: [LegacyChartsModule], // un NgModule dans les imports d’un composant standalone — autorisé
template: `<legacy-chart [data]="data()" />`,
})
export class Report {}
// Un module qui doit ajouter des providers au niveau de l’application :
export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(LegacyAnalyticsModule.forRoot({ key: '...' }))],
};Migrer un projet existant
# Migration automatique vers standalone (à lancer en 3 étapes, comme le demande le schematic)
ng generate @angular/core:standalone
# Autres migrations utiles pour un ancien projet (toutes présentes dans @angular/core v22)
ng generate @angular/core:control-flow # *ngIf/*ngFor/ngSwitch → @if/@for/@switch
ng generate @angular/core:common-to-standalone # CommonModule → imports individuels
ng generate @angular/core:inject # constructor DI → inject()
ng generate @angular/core:signals # @Input/@Output/@ViewChild → input()/output()/viewChild()
ng generate @angular/core:ngclass-to-class # [ngClass] → [class]
ng generate @angular/core:ngstyle-to-style # [ngStyle] → [style]
ng generate @angular/core:service # @Injectable({providedIn:'root'}) → @Service() (v22)
ng generate @angular/core:route-lazy-loading # component: X → loadComponent: () => import(...)
ng generate @angular/core:self-closing-tag # <app-x></app-x> → <app-x />
ng generate @angular/core:cleanup-unused-importsQuand OUI, quand NON
- Tout nouveau code : composants, directives, pipes.
- Lazy loading au niveau du composant (
loadComponent) ou d’un groupe de routes (loadChildrenavec un fichier de routes). - Configuration via des fonctions
provideX()au lieu deModule.forRoot().
- Vous maintenez une bibliothèque publique qui doit prendre en charge de très anciennes versions d’Angular.
- Vous avez une grande base de code pas encore migrée — vous migrez progressivement, les deux modèles coexistent.
Pièges fréquents
- « X is not a known element » : vous avez oublié d’ajouter le composant à
imports. Chaque composant importe ses propres enfants. - Unknown pipe :
daten’est plus « gratuit » — importezDatePipe. standalone: falsen’apparaît que dans du code ancien ou migré. Ne l’écrivez pas dans du nouveau code.- Les providers que vous mettez dans
app.config.tssont globaux. Pour une seule fonctionnalité, mettez-les sur la route (voir Providers).