Capítulo 4 - Parte 3: Componentes Standalone: el futuro de Angular¶
Parte 3 de 4 · Capítulo 4 · PARTE II - Componentes: El Alma de Angular
Desde sus primeras versiones, Angular organizó los componentes en NgModules. Cada componente debía pertenecer a exactamente un módulo que declarara sus dependencias, los exportara para que otros módulos los usaran, y participara en la cadena de resolución del inyector. Este sistema funciona, pero tiene un precio: añade una capa de indirección que dificulta entender qué necesita un componente para funcionar sin leer dos archivos a la vez. Los componentes standalone eliminan esa indirección.
Qué cambia con standalone: true¶
La diferencia fundamental es que un componente standalone es su propio descriptor de dependencias. En lugar de delegar en un NgModule que declare qué pipes, directivas y componentes están disponibles, el propio componente los lista en su decorador. El resultado es que para entender qué usa un componente, basta con leer su archivo.
Con el enfoque de módulos:
// app.module.ts - había que ir aquí para entender qué tiene disponible el componente
@NgModule({
declarations: [ProductoListaComponent, ProductoTarjetaComponent],
imports: [CommonModule, RouterModule, MatButtonModule],
exports: [ProductoListaComponent]
})
export class ProductosModule {}
// producto-lista.component.ts - no dice nada sobre sus dependencias
@Component({ selector: 'app-producto-lista', templateUrl: '...' })
export class ProductoListaComponent {}
Con el enfoque standalone:
// producto-lista.component.ts - todo lo necesario está aquí
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { ProductoTarjetaComponent } from './producto-tarjeta.component';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-producto-lista',
standalone: true,
// Todo lo que el template necesita está declarado aquí
imports: [RouterLink, MatButtonModule, ProductoTarjetaComponent, CurrencyPipe],
templateUrl: './producto-lista.component.html'
})
export class ProductoListaComponent {}
El componente es completamente autónomo. Puede moverse a otro proyecto, a otra carpeta, o ser usado en otro componente sin tener que actualizar ningún módulo.
El array imports del decorador¶
En un componente standalone, el array imports acepta:
- Otros componentes standalone: simplemente se importan directamente.
- Directivas y pipes standalone: como
NgClass,AsyncPipe,CurrencyPipede@angular/common. - NgModules: un componente standalone puede importar un NgModule completo para acceder a todo lo que exporta. Esto facilita la adopción progresiva: puedes usar librerías como Angular Material (que aún exporta módulos como
MatButtonModule) sin necesidad de que el ecosistema completo sea standalone.
import { Component } from '@angular/core';
import { NgClass, AsyncPipe, DatePipe } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card'; // NgModule de Material
import { RouterLink, RouterOutlet } from '@angular/router';
import { MiComponenteStandalone } from './mi-componente.component'; // Standalone
@Component({
selector: 'app-ejemplo-imports',
standalone: true,
imports: [
// Directivas/pipes individuales de @angular/common
NgClass,
AsyncPipe,
DatePipe,
// Módulo completo de Angular Forms
ReactiveFormsModule,
// NgModule de Material (aún no todos son standalone)
MatCardModule,
// Primitivas del router
RouterLink,
RouterOutlet,
// Componente standalone del proyecto
MiComponenteStandalone,
],
template: `<!-- Template con acceso a todo lo anterior -->`
})
export class EjemploImportsComponent {}
Arrancando la aplicación sin AppModule¶
El cambio más visible cuando se abandona el enfoque de módulos es en el punto de entrada de la aplicación. En lugar de platformBrowserDynamic().bootstrapModule(AppModule), se usa bootstrapApplication() con el componente raíz:
// main.ts - punto de entrada moderno sin NgModule
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { rutas } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
// El router se configura aquí con funciones de configuración
provideRouter(rutas),
// HttpClient disponible en toda la aplicación
provideHttpClient(),
// Cualquier servicio con providedIn: 'root' se registra automáticamente
// Los demás pueden declararse aquí
]
})
.catch(err => console.error(err));
AppComponent debe ser standalone. Las funciones provideRouter(), provideHttpClient(), provideAnimations() y similares son el equivalente standalone de importar RouterModule.forRoot(), HttpClientModule y demás en AppModule.
Ventajas concretas sobre NgModules¶
Tree shaking más agresivo: cuando las dependencias se declaran por componente, el compilador de Angular puede determinar con mayor precisión qué código se usa y qué no. Si un componente no usa DatePipe, no se incluye en el bundle de ese componente, aunque otro sí la use.
Testabilidad mejorada: en los tests unitarios, ya no es necesario configurar un TestBed con el módulo completo. Se puede importar el componente directamente con solo sus dependencias reales o sus mocks:
// Configuración de test con componente standalone
await TestBed.configureTestingModule({
imports: [
ProductoTarjetaComponent, // El componente a testear
provideHttpClientTesting(), // Mock de HttpClient
]
}).compileComponents();
Carga diferida más granular: un componente standalone puede ser cargado de forma diferida directamente en las rutas del router, sin necesidad de crear un módulo de feature solo para envolver el componente.
Portabilidad: un componente standalone con todos sus imports declarados puede copiarse entre proyectos con mínimo esfuerzo. No hay que rastrear en qué módulo estaba declarado y qué exportaba ese módulo.
Migración de un componente basado en módulo a standalone¶
El proceso de migración es mecánico y predecible. Angular CLI ofrece un schematic automatizado, pero es útil entender los pasos manuales:
Paso 1 - Añadir standalone: true al decorador y declarar los imports en el propio componente:
// ANTES: componente dependiente de módulo
// En usuario-perfil.module.ts:
// declarations: [UsuarioPerfilComponent]
// imports: [CommonModule, ReactiveFormsModule]
// DESPUÉS: componente standalone
import { Component } from '@angular/core';
import { NgIf, NgFor } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-usuario-perfil',
standalone: true, // Paso 1: añadir standalone
imports: [NgIf, NgFor, ReactiveFormsModule], // Paso 2: mover imports aquí
templateUrl: './usuario-perfil.component.html',
styleUrl: './usuario-perfil.component.css'
})
export class UsuarioPerfilComponent {
// ... lógica sin cambios
}
Paso 2 - Eliminar el componente de las declarations del NgModule. Si era el único componente del módulo, el módulo puede eliminarse por completo.
Paso 3 - Donde se usaba el NgModule (en imports de otros módulos o del AppModule), importar directamente el componente standalone.
Angular CLI automatiza esto con:
# Migra todos los componentes de la aplicación a standalone de forma automática
ng generate @angular/core:standalone
# O en modo interactivo para elegir qué migrar
ng generate @angular/core:standalone --mode=convert-to-standalone
Puntos clave¶
standalone: truehace que el componente gestione sus propias dependencias en el arrayimports, eliminando la necesidad de un NgModule intermediario.- El array
importsacepta componentes standalone, directivas, pipes y NgModules completos mezclados. bootstrapApplication()reemplaza aplatformBrowserDynamic().bootstrapModule()en aplicaciones sin NgModules.- Los componentes standalone permiten lazy loading directo en el router y mejoran el tree shaking del bundle final.
- La migración es progresiva: un componente standalone puede importar NgModules existentes, y un NgModule puede importar componentes standalone.
¿Qué sigue?¶
En la Parte 4 cerramos el capítulo con Deferrable Views: la capacidad de Angular 17+ para cargar fragmentos de UI de forma diferida, controlando exactamente cuándo y bajo qué condición se descarga el JavaScript de un componente pesado.