Standalone Components: Angular senza NgModules
Con l'introduzione degli Standalone Components in Angular 14 e la loro progressiva adozione come standard a partire da Angular 15, il framework ha compiuto una svolta architetturale fondamentale: l'eliminazione dell'obbligo di utilizzare NgModules per organizzare il codice. Questa evoluzione ha semplificato drasticamente lo sviluppo, ridotto il boilerplate e reso Angular molto più accessibile ai nuovi sviluppatori.
In questo quarto articolo della serie Angular Moderno esploreremo gli Standalone
Components in profondità: dal problema che risolvono alla migrazione completa, passando per il
routing, il bootstrapping, la dependency injection e i pattern avanzati. Se hai mai lottato con
declarations, imports e exports nei moduli Angular,
questo articolo ti mostrera come liberartene definitivamente.
Cosa Imparerai
- perchè i NgModules rappresentavano un problema e come gli Standalone Components lo risolvono
- Come creare componenti, direttive e pipe standalone
- Gestire le dipendenze con
importsdirettamente nel componente - Configurare routing e lazy loading senza moduli
- Bootstrapping dell'applicazione con
bootstrapApplication() - Dependency Injection nel mondo standalone
- Migrare un progetto esistente con lo schematic ufficiale
- Pattern avanzati e best practice per applicazioni enterprise
Panoramica della Serie Angular Moderno
| # | Articolo | Focus |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Zoneless Change Detection | Eliminare Zone.js |
| 3 | Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Sei qui - Standalone Components | Architettura senza NgModule |
| 5 | Signal Forms | Form reattivi con Signals |
| 6 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Angular PWA | Progressive Web App |
| 9 | Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida alla migrazione |
1. Il Problema dei NgModules
Sin dalla versione 2, Angular ha utilizzato i NgModules come unita fondamentale
di organizzazione del codice. Ogni componente, direttiva o pipe doveva essere dichiarato
esattamente in un modulo tramite l'array declarations, e le dipendenze tra moduli
venivano gestite tramite imports e exports. Questo sistema, pur
fornendo una struttura chiara, ha generato problemi crescenti man mano che le applicazioni
diventavano più complesse.
// app.module.ts - L'approccio tradizionale
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { ProductListComponent } from './products/product-list.component';
import { ProductDetailComponent } from './products/product-detail.component';
import { CartComponent } from './cart/cart.component';
import { CheckoutComponent } from './checkout/checkout.component';
import { HighlightDirective } from './directives/highlight.directive';
import { CurrencyPipe } from './pipes/currency.pipe';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
HomeComponent,
ProductListComponent,
ProductDetailComponent,
CartComponent,
CheckoutComponent,
HighlightDirective,
CurrencyPipe
],
imports: [
BrowserModule,
HttpClientModule,
ReactiveFormsModule,
RouterModule.forRoot(routes)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Problemi Comuni con i NgModules
- Boilerplate eccessivo: Ogni componente richiede l'importazione e la dichiarazione nel modulo. In progetti grandi si arriva a centinaia di righe solo per le dichiarazioni
- Dipendenze circolari: Quando due moduli si importano a vicenda si creano errori difficili da diagnosticare
- Confusione declarations vs imports: I nuovi sviluppatori confondono costantemente dove mettere componenti (declarations) e dove mettere moduli (imports)
- Errore "Component is not part of any NgModule": Uno degli errori più frustranti per i principianti, causato dalla dimenticanza di dichiarare un componente
- Accoppiamento: I componenti sono accoppiati al modulo che li dichiara, rendendo difficile il riutilizzo e il test unitario
- Lazy loading complesso: Per caricare una feature in modo lazy bisogna creare un modulo dedicato con la sua configurazione di routing
Consideriamo un esempio tipico di dipendenza circolare che affliggeva molti progetti Angular:
// shared.module.ts
@NgModule({
declarations: [UserAvatarComponent],
imports: [ProductModule], // Importa ProductModule
exports: [UserAvatarComponent]
})
export class SharedModule { }
// product.module.ts
@NgModule({
declarations: [ProductCardComponent],
imports: [SharedModule], // Importa SharedModule → CIRCOLARE!
exports: [ProductCardComponent]
})
export class ProductModule { }
NgModules vs Standalone: Confronto Rapido
| Aspetto | NgModules | Standalone |
|---|---|---|
| Dichiarazione componenti | In declarations del modulo |
Self-contained con standalone: true |
| Gestione dipendenze | Tramite moduli intermedi | Direttamente nel componente |
| Lazy loading | Richiede NgModule dedicato | loadComponent() diretto |
| Boilerplate | Alto (modulo + dichiarazioni) | Minimo |
| Test unitari | TestBed con modulo di test | Configurazione minima |
| Dipendenze circolari | Frequenti tra moduli | Quasi impossibili |
| Curva di apprendimento | Ripida (declarations, imports, exports, providers, bootstrap) | Dolce (standalone: true, imports) |
2. Cosa sono gli Standalone Components
Uno Standalone Component è un componente Angular che dichiara se stesso come
autosufficiente tramite il flag standalone: true nel decoratore
@Component. Questo significa che il componente non ha bisogno di essere dichiarato
in nessun NgModule: gestisce le proprie dipendenze direttamente nel suo array imports.
A partire da Angular 19, tutti i componenti, le direttive e le pipe sono
standalone per default. Non serve nemmeno specificare standalone: true
perchè è il comportamento predefinito. Questa è la dimostrazione definitiva che il team Angular
considera gli Standalone Components il futuro del framework.
// Angular 14-18: standalone esplicito
@Component({
selector: 'app-user-card',
standalone: true,
template: `<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
</div>`,
styles: [`
.user-card {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserCardComponent {
user = input.required<User>();
}
// Angular 19+: standalone e il default, non serve specificarlo
@Component({
selector: 'app-user-card',
template: `<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
</div>`,
styles: [`
.user-card {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserCardComponent {
user = input.required<User>();
}
Nota su standalone: true in Angular 19+
Da Angular 19, se ometti la proprietà standalone, il componente è standalone
per default. Se vuoi esplicitamente creare un componente non-standalone (per compatibilità
con codice legacy), devi specificare standalone: false. In questo
articolo useremo sempre standalone: true per chiarezza, ma
ricorda che in Angular 21 e opzionale.
3. Creare Componenti, Direttive e Pipe Standalone
Il concetto di "standalone" non si applica solo ai componenti. Anche le direttive e le pipe possono essere standalone, seguendo lo stesso principio di autosufficienza.
Componente Standalone
import { Component, input, output, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule], // Dipendenze gestite qui
template: `
<div class="todo-item" [class.completed]="todo().completed">
<input type="checkbox"
[checked]="todo().completed"
(change)="onToggle()" />
<span>{{ todo().title }}</span>
<button (click)="onDelete()">Elimina</button>
</div>
`
})
export class TodoItemComponent {
todo = input.required<Todo>();
toggled = output<number>();
deleted = output<number>();
onToggle() {
this.toggled.emit(this.todo().id);
}
onDelete() {
this.deleted.emit(this.todo().id);
}
}
Direttiva Standalone
import { Directive, ElementRef, input, effect } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
appHighlight = input<string>('yellow');
constructor(private el: ElementRef) {
effect(() => {
this.el.nativeElement.style.backgroundColor = this.appHighlight();
});
}
}
Pipe Standalone
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'timeAgo',
standalone: true
})
export class TimeAgoPipe implements PipeTransform {
transform(value: Date): string {
const seconds = Math.floor((new Date().getTime() - value.getTime()) / 1000);
if (seconds < 60) return 'pochi secondi fa';
if (seconds < 3600) return Math.floor(seconds / 60) + ' minuti fa';
if (seconds < 86400) return Math.floor(seconds / 3600) + ' ore fa';
return Math.floor(seconds / 86400) + ' giorni fa';
}
}
4. Gestire le Dipendenze con imports
La chiave degli Standalone Components è l'array imports nel decoratore
@Component. Qui si dichiarano tutte le dipendenze del template:
altri componenti, direttive, pipe e anche NgModules interi (per compatibilità con librerie
legacy). Ogni componente sa esattamente di cosa ha bisogno, senza intermediari.
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { AsyncPipe, DatePipe, CurrencyPipe } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
// Importa componenti standalone direttamente
import { UserCardComponent } from './user-card.component';
import { LoadingSpinnerComponent } from '../shared/loading-spinner.component';
import { HighlightDirective } from '../directives/highlight.directive';
import { TimeAgoPipe } from '../pipes/time-ago.pipe';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
// Moduli Angular (per compatibilità)
ReactiveFormsModule,
// Pipe standalone
AsyncPipe,
DatePipe,
CurrencyPipe,
TimeAgoPipe,
// Componenti standalone
UserCardComponent,
LoadingSpinnerComponent,
// Direttive standalone
HighlightDirective,
// Router
RouterLink,
RouterOutlet
],
templateUrl: './dashboard.component.html'
})
export class DashboardComponent {
// Il componente conosce tutte le sue dipendenze
}
Confronto: Dipendenze con NgModules vs Standalone
| Con NgModules | Con Standalone |
|---|---|
| Componente dichiarato in un modulo | Componente autosufficiente |
| Dipendenze importate nel modulo | Dipendenze importate nel componente |
| Tutte le dichiarazioni del modulo vedono le importazioni | Solo il componente vede le sue importazioni |
| Rischio di importazioni non utilizzate nel modulo | Ogni importazione è esplicita e necessaria |
Attenzione: Non importare più del necessario
Con gli Standalone Components potresti essere tentato di importare CommonModule
intero per comodita. Tuttavia, è meglio importare solo le singole pipe e direttive di cui
hai bisogno (es. NgIf, NgFor, AsyncPipe).
Con la nuova sintassi @if e @for di Angular 17+,
molte di queste importazioni non sono più necessarie perchè il control flow è nativo nel template.
5. Routing senza NgModules
Uno dei maggiori vantaggi degli Standalone Components è la semplificazione del routing.
Con i moduli, per caricare una feature in modo lazy bisognava creare un modulo dedicato con
RouterModule.forChild(). Con gli Standalone Components, si può fare lazy loading
di un singolo componente direttamente con loadComponent.
loadComponent: Lazy Loading di un Singolo Componente
// app.routes.ts - Configurazione routing completa senza moduli
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'products',
loadComponent: () =>
import('./products/product-list.component').then(m => m.ProductListComponent)
},
{
path: 'products/:id',
loadComponent: () =>
import('./products/product-detail.component').then(m => m.ProductDetailComponent)
},
{
path: 'cart',
loadComponent: () =>
import('./cart/cart.component').then(m => m.CartComponent)
},
{
path: '**',
loadComponent: () =>
import('./not-found/not-found.component').then(m => m.NotFoundComponent)
}
];
loadChildren: Lazy Loading di un Gruppo di Route
Per feature complesse con sotto-route, si può usare loadChildren puntando a un
file che esporta un array di Routes, senza bisogno di un modulo wrapper.
// app.routes.ts
export const routes: Routes = [
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
}
];
// admin/admin.routes.ts - File di route standalone
import { Routes } from '@angular/router';
export const ADMIN_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./admin-layout.component').then(m => m.AdminLayoutComponent),
children: [
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component').then(m => m.DashboardComponent)
},
{
path: 'users',
loadComponent: () =>
import('./users/users.component').then(m => m.UsersComponent)
},
{
path: 'settings',
loadComponent: () =>
import('./settings/settings.component').then(m => m.SettingsComponent)
}
]
}
];
Lazy Loading: NgModules vs Standalone
| Approccio | NgModules | Standalone |
|---|---|---|
| Lazy load singolo componente | Impossibile senza modulo wrapper | loadComponent: () => import(...) |
| Lazy load feature | loadChildren con NgModule |
loadChildren con array Routes |
| File necessari | feature.module.ts + feature-routing.module.ts | feature.routes.ts (solo) |
| Granularità | A livello di modulo | A livello di singolo componente |
6. Bootstrapping con bootstrapApplication()
Con gli Standalone Components, anche il modo in cui si avvia l'applicazione cambia radicalmente.
Non serve più un AppModule con il metodo platformBrowserDynamic().bootstrapModule().
Si utilizza invece bootstrapApplication(), che accetta il componente root e una
configurazione di provider.
Il Vecchio Approccio con NgModule
// main.ts - Approccio tradizionale
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
Il Nuovo Approccio Standalone
// main.ts - Approccio standalone
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.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
// Change detection (o provideExperimentalZonelessChangeDetection())
provideZoneChangeDetection({ eventCoalescing: true }),
// Router con view transitions
provideRouter(routes, withViewTransitions()),
// HTTP client con interceptors funzionali
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor, errorInterceptor])
),
// Animazioni asincrone
provideAnimationsAsync(),
// Hydration per SSR
provideClientHydration(withEventReplay())
]
};
Le Funzioni provide*
Con l'approccio standalone, Angular introduce le funzioni provide* che sostituiscono i vecchi moduli di configurazione. Ecco le principali:
| Funzione provide* | Sostituisce |
|---|---|
provideRouter() |
RouterModule.forRoot() |
provideHttpClient() |
HttpClientModule |
provideAnimationsAsync() |
BrowserAnimationsModule |
provideClientHydration() |
Configurazione SSR nel modulo |
provideZoneChangeDetection() |
Configurazione implicita di Zone.js |
7. Dependency Injection nel Mondo Standalone
La Dependency Injection (DI) di Angular funziona perfettamente con gli Standalone Components, ma offre nuovi pattern per configurare i provider a diversi livelli dell'applicazione.
providedIn: 'root' - Il Pattern Consigliato
Il modo più semplice e raccomandato per fornire un servizio è utilizzare
providedIn: 'root' nel decoratore @Injectable.
Questo registra il servizio come singleton nell'injector root, disponibile ovunque
nell'applicazione. Il vantaggio è che il servizio è tree-shakable: se nessun componente
lo inietta, viene rimosso dal bundle di produzione.
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root' // Singleton tree-shakable
})
export class CartService {
private _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly totalItems = computed(() => this._items().length);
readonly totalPrice = computed(() =>
this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
addItem(product: Product) {
this._items.update(items => [...items, {
...product,
quantity: 1
}]);
}
removeItem(id: number) {
this._items.update(items => items.filter(item => item.id !== id));
}
}
Provider a Livello di Route
Per servizi che devono essere condivisi solo all'interno di una feature, si possono definire i provider direttamente nella configurazione della route. Questo crea un environment injector dedicato a quella sezione dell'applicazione.
// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{
path: '',
providers: [
// Questo servizio è disponibile SOLO nei componenti admin
AdminAuthService,
// Anche i provider configurabili funzionano
{
provide: API_BASE_URL,
useValue: '/api/admin'
}
],
children: [
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard.component').then(m => m.DashboardComponent)
},
{
path: 'users',
loadComponent: () =>
import('./users.component').then(m => m.UsersComponent)
}
]
}
];
Provider a Livello di Componente
@Component({
selector: 'app-editor',
standalone: true,
providers: [
// Nuova istanza per ogni componente editor
EditorStateService,
{
provide: UNDO_STACK_SIZE,
useValue: 50
}
],
template: `...`
})
export class EditorComponent {
private editorState = inject(EditorStateService);
}
Gerarchia dei Provider nel Mondo Standalone
| Livello | Configurazione | Scope | Caso d'uso |
|---|---|---|---|
| Root | providedIn: 'root' |
Tutta l'app (singleton) | AuthService, HttpService, StateStore |
| App Config | appConfig.providers |
Tutta l'app | Router, HttpClient, Animations |
| Route | route.providers |
Feature/sezione | AdminService, FeatureConfig |
| Componente | component.providers |
Singolo componente | FormState, EditorState |
8. Migrare da NgModules a Standalone
Angular fornisce uno schematic ufficiale di migrazione che automatizza gran parte del processo di conversione da NgModules a Standalone Components. La migrazione avviene in tre fasi progressive, permettendo di procedere in modo sicuro e incrementale.
Fase 1: Convertire i Componenti in Standalone
Il primo passo converte tutti i componenti, le direttive e le pipe in standalone, aggiungendo
standalone: true e gli imports necessari.
# Fase 1: Converti componenti, direttive e pipe in standalone
ng generate @angular/core:standalone-migration --mode=convert-to-standalone
# Cosa fa:
# - Aggiunge standalone: true a tutti i componenti/direttive/pipe
# - Aggiunge gli imports necessari nel decoratore di ogni componente
# - Mantiene i NgModules esistenti per compatibilità
Fase 2: Rimuovere i NgModules non Necessari
# Fase 2: Rimuovi i NgModules che non sono più necessari
ng generate @angular/core:standalone-migration --mode=prune-ng-modules
# Cosa fa:
# - Rimuove i NgModules che contengono solo declarations standalone
# - Aggiorna le importazioni nei file che li riferivano
# - Mantiene i moduli che definiscono providers o altro
Fase 3: Passare a bootstrapApplication()
# Fase 3: Converti il bootstrap dell'applicazione
ng generate @angular/core:standalone-migration --mode=standalone-bootstrap
# Cosa fa:
# - Converte main.ts per usare bootstrapApplication()
# - Crea app.config.ts con i provider
# - Rimuove AppModule se non più necessario
# - Migra i provider dal modulo alla configurazione
Consigli per una Migrazione Sicura
- Fai un commit prima di ogni fase: Cosi puoi facilmente tornare indietro se qualcosa va storto
- Esegui i test dopo ogni fase: Assicurati che l'applicazione funzioni correttamente prima di procedere
- Verifica le dipendenze circolari: La migrazione potrebbe evidenziare dipendenze circolari nascoste nei moduli
- Controlla i provider: Alcuni moduli definiscono provider con
forRoot()eforChild()che richiedono attenzione manuale - Librerie di terze parti: Verifica che le librerie che usi supportino standalone. La maggior parte lo fa esportando i componenti come standalone o fornendo funzioni
provide*()
Migrazione Manuale Passo-Passo
Se preferisci migrare manualmente, ecco il processo tipico per convertire un componente:
// PRIMA: product-card.component.ts (in un NgModule)
@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
@Input() product!: Product;
@Output() addToCart = new EventEmitter<Product>();
}
// product.module.ts
@NgModule({
declarations: [ProductCardComponent, ProductListComponent],
imports: [CommonModule, RouterModule],
exports: [ProductCardComponent]
})
export class ProductModule { }
// DOPO: product-card.component.ts (standalone)
import { Component, input, output } from '@angular/core';
import { RouterLink } from '@angular/router';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [RouterLink, CurrencyPipe],
templateUrl: './product-card.component.html'
})
export class ProductCardComponent {
product = input.required<Product>();
addToCart = output<Product>();
}
// product.module.ts → ELIMINATO!
// Le route puntano direttamente ai componenti
9. Pattern Avanzati
Barrel Exports per Componenti Condivisi
Quando si hanno molti componenti standalone condivisi, è utile creare un file barrel
(index.ts) che li riesporta tutti. Questo semplifica le importazioni
nei componenti che li utilizzano.
// shared/ui/index.ts - Barrel export
export { ButtonComponent } from './button/button.component';
export { CardComponent } from './card/card.component';
export { ModalComponent } from './modal/modal.component';
export { TabsComponent } from './tabs/tabs.component';
export { TooltipDirective } from './tooltip/tooltip.directive';
export { TimeAgoPipe } from './pipes/time-ago.pipe';
export { TruncatePipe } from './pipes/truncate.pipe';
// Utilizzo nel componente:
import {
ButtonComponent,
CardComponent,
TooltipDirective,
TruncatePipe
} from '../shared/ui';
@Component({
standalone: true,
imports: [ButtonComponent, CardComponent, TooltipDirective, TruncatePipe],
// ...
})
export class MyFeatureComponent { }
Comporre Array di Importazioni Comuni
Se più componenti condividono le stesse dipendenze, si può creare un array di importazioni comuni da riutilizzare. Questo è un pattern leggero alternativo ai SharedModule.
// shared/common-imports.ts
import { RouterLink } from '@angular/router';
import { DatePipe, CurrencyPipe, AsyncPipe } from '@angular/common';
import { ButtonComponent } from './ui/button/button.component';
import { CardComponent } from './ui/card/card.component';
import { LoadingSpinnerComponent } from './ui/loading-spinner/loading-spinner.component';
export const COMMON_IMPORTS = [
RouterLink,
DatePipe,
CurrencyPipe,
AsyncPipe,
ButtonComponent,
CardComponent,
LoadingSpinnerComponent
] as const;
// Utilizzo:
@Component({
standalone: true,
imports: [...COMMON_IMPORTS, ReactiveFormsModule],
// ...
})
export class CheckoutComponent { }
Lazy Loading Granulare con @defer
Combinando gli Standalone Components con il blocco @defer introdotto in Angular 17,
si può ottenere un lazy loading ancora più granulare a livello di template:
<!-- Il componente HeavyChart viene caricato solo quando entra nel viewport -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="chart-placeholder">Grafico in caricamento...</div>
} @loading (minimum 500ms) {
<app-loading-spinner />
}
<!-- Il componente Comments viene caricato solo all'interazione -->
@defer (on interaction) {
<app-comments [postId]="postId()" />
} @placeholder {
<button>Mostra commenti</button>
}
Vantaggi del Lazy Loading Granulare
Con i NgModules, la granularità minima del lazy loading era un intero modulo con
tutti i suoi componenti. Con gli Standalone Components, ogni singolo componente può
essere caricato in modo indipendente, sia tramite il router (loadComponent)
che tramite il template (@defer). Questo riduce significativamente il
bundle iniziale e migliora i tempi di caricamento.
10. Best Practices
Dopo aver esplorato tutte le funzionalità degli Standalone Components, ecco le best practice da seguire per organizzare al meglio il codice nelle applicazioni Angular moderne.
Le 10 Regole d'Oro degli Standalone Components
- Standalone per default: Crea sempre componenti standalone. Usa NgModules solo per compatibilità con librerie legacy
- Importa solo ciò che serve: Evita di importare interi moduli come
CommonModule. Importa le singole pipe e direttive - Usa la nuova sintassi di template: Con
@if,@fore@switchnon hai più bisogno di importareNgIf,NgFor,NgSwitch - Un componente, un file: Ogni componente standalone dovrebbe avere il proprio file. Evita di dichiarare più componenti nello stesso file
- Barrel exports per le feature: Usa file
index.tsper riesportare i componenti condivisi di una feature - Provider nelle route, non nei componenti: Per servizi di feature, preferisci
route.providersrispetto acomponent.providers - providedIn: 'root' per i servizi globali: Mantieni
providedIn: 'root'per servizi singleton e tree-shakable - Lazy loading aggressivo: Usa
loadComponentnel router e@defernel template per caricare i componenti on-demand - Migra gradualmente: Non devi migrare tutto in una volta. Standalone e NgModules possono coesistere indefinitamente
- Testa ogni componente in isolamento: Un grande vantaggio degli standalone e la facilità di testing. Sfruttala!
Struttura di Progetto Consigliata
src/app/
app.component.ts # Root component (standalone)
app.config.ts # ApplicationConfig con providers
app.routes.ts # Route principali
core/ # Servizi singleton e guard
services/
auth.service.ts # providedIn: 'root'
api.service.ts # providedIn: 'root'
interceptors/
auth.interceptor.ts
guards/
auth.guard.ts
shared/ # Componenti, direttive, pipe riutilizzabili
ui/
button/
button.component.ts # standalone
card/
card.component.ts # standalone
modal/
modal.component.ts # standalone
index.ts # barrel export
directives/
tooltip.directive.ts # standalone
pipes/
time-ago.pipe.ts # standalone
features/ # Feature dell'applicazione
products/
product-list.component.ts # standalone
product-detail.component.ts # standalone
product-card.component.ts # standalone (locale)
products.routes.ts # sotto-route
admin/
admin-layout.component.ts
dashboard.component.ts
admin.routes.ts
Testing di Componenti Standalone
Uno dei vantaggi più significativi è la semplificazione del testing. Non serve più configurare
un modulo di test complesso con TestBed: il componente porta con se tutte le
sue dipendenze.
// product-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductCardComponent } from './product-card.component';
describe('ProductCardComponent', () => {
let fixture: ComponentFixture<ProductCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
// Con standalone, basta importare il componente!
imports: [ProductCardComponent]
// Non serve: declarations, imports di moduli, etc.
}).compileComponents();
fixture = TestBed.createComponent(ProductCardComponent);
});
it('dovrebbe mostrare il nome del prodotto', () => {
// Usa ComponentRef per impostare gli input
fixture.componentRef.setInput('product', {
id: 1,
name: 'Angular T-Shirt',
price: 29.99
});
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.textContent).toContain('Angular T-Shirt');
});
it('dovrebbe emettere addToCart quando si clicca il pulsante', () => {
fixture.componentRef.setInput('product', {
id: 1,
name: 'Angular T-Shirt',
price: 29.99
});
fixture.detectChanges();
const spy = jest.spyOn(fixture.componentInstance.addToCart, 'emit');
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(spy).toHaveBeenCalledWith(expect.objectContaining({
id: 1
}));
});
});
Quando Mantenere i NgModules
Anche se gli Standalone Components sono il futuro, ci sono casi in cui i NgModules sono ancora utili o necessari:
- Librerie di terze parti: Alcune librerie esportano ancora NgModules. Puoi importarli direttamente nell'array
importsdel componente standalone - Progetti legacy di grandi dimensioni: Se hai un progetto con centinaia di componenti, la migrazione può essere graduale. Non serve fare tutto subito
- SharedModule con molte dipendenze: Se hai uno SharedModule che importa decine di componenti, potrebbe essere più pratico mantenerlo temporaneamente e sostituirlo con un barrel export quando sei pronto
Riepilogo e Prossimi Passi
In questo articolo abbiamo esplorato in profondità gli Standalone Components di Angular: dalla motivazione alla migrazione, dal routing alla dependency injection. Ecco i punti chiave:
Concetti Chiave
- Gli Standalone Components eliminano la necessità di NgModules, rendendo ogni componente autosufficiente
- Da Angular 19, standalone è il comportamento di default per componenti, direttive e pipe
- L'array
importsnel decoratore permette di dichiarare le dipendenze direttamente nel componente loadComponenteloadChildrensemplificano il lazy loading senza moduli wrapperbootstrapApplication()eApplicationConfigsostituiscono il bootstrap tramite NgModule- Le funzioni
provide*()sostituiscono i moduli di configurazione (RouterModule.forRoot(), etc.) - La DI funziona a quattro livelli: root, app config, route e componente
- Lo schematic di migrazione automatizza la conversione in tre fasi progressive
- I barrel exports e gli array di importazioni comuni sostituiscono gli SharedModule
Nel prossimo articolo della serie, Signal Forms, vedremo come la rivoluzione dei Signals si estende alla gestione dei form, con un'alternativa moderna ai Reactive Forms che sfrutta la reattività fine-grained per creare form performanti e dichiarativi.
Domande frequenti
Cos'è il problema principale dei NgModules in Angular?
I NgModules possono generare problemi crescenti man mano che le applicazioni diventano più complesse, come ad esempio dipendenze circolari tra moduli.
Cosa sono gli Standalone Components in Angular?
Gli Standalone Components sono componenti Angular autosufficienti che dichiarano se stessi tramite il flag nel decoratore e gestiscono le proprie dipendenze direttamente nel loro array.
Come si possono creare componenti, direttive e pipe standalone in Angular?
Si possono creare componenti, direttive e pipe standalone utilizzando lo stesso principio di autosufficienza del decoratore .
Qual è il vantaggio principale degli Standalone Components nel routing in Angular?
Il vantaggio principale degli Standalone Components nel routing in Angular è la semplificazione del routing, che consente di caricare una feature in modo lazy senza dover creare un modulo dedicato.

Commenti
Caricamento commenti...
Accedi per commentare