Componentes Standalone en Angular: Migración y Buenas Prácticas para Angular 22

Guía completa para migrar aplicaciones Angular de NgModules a componentes standalone. El schematic CLI oficial en 3 pasos, componentes sin selector, OnPush por defecto, y lazy loading con loadComponent en Angular 22.

Angular Standalone Components Migration

Los componentes standalone de Angular eliminan la necesidad de NgModules, reduciendo el boilerplate y habilitando lazy loading granular en toda la aplicación. Desde que Angular 19 estableció standalone como valor predeterminado y Angular 22 introdujo los componentes sin selector (selectorless) como estables, la migración de bases de código heredadas basadas en módulos se ha vuelto directa y de alto impacto.

Punto clave

El schematic oficial del CLI de Angular maneja la mayor parte de la migración automáticamente en tres pasadas. Una aplicación empresarial típica puede completar la conversión en un solo sprint, con reducciones de tamaño de bundle del 30-50% gracias al lazy loading por componente.

NgModules vs Componentes Standalone: Qué cambió

Los NgModules sirvieron como contexto de compilación para componentes desde Angular 2. Cada componente, directiva y pipe debía ser declarado en exactamente un módulo, y la funcionalidad compartida requería importaciones y exportaciones de módulos cuidadosamente orquestadas. Esto creaba acoplamiento estrecho entre características no relacionadas y dificultaba el tree-shaking.

Los componentes standalone invierten este modelo. Cada componente declara sus propias dependencias directamente en el array imports del decorador @Component. Sin registro de módulos, sin SharedModules, sin barrel exports de la mitad de la aplicación. A partir de Angular 22, este patrón se simplifica aún más con componentes sin selector, permitiendo importaciones directas sin selectores de texto.

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

El array imports reemplaza todo el grafo de dependencias de NgModules. El bundler ve exactamente qué componentes, pipes y directivas necesita cada archivo, habilitando tree-shaking preciso. CommonModule ya no es necesario en Angular 22 porque la sintaxis de flujo de control (@for, @if) está integrada en el compilador.

El proceso de migración CLI en 3 pasos

Angular proporciona un schematic automatizado que maneja la migración en tres pasadas secuenciales. Cada paso se basa en el anterior, y el proyecto debe compilar correctamente entre cada pasada.

Paso 1: Convertir declaraciones a standalone

La primera pasada analiza cada componente, directiva y pipe del proyecto, agrega standalone: true, y mueve las importaciones necesarias desde su NgModule padre al array imports de cada componente.

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

Seleccionar "Convert all components, directives and pipes to standalone" cuando aparezca el prompt. El schematic usa análisis estático para resolver dependencias, cualquier componente con metadata que no pueda ser analizada en tiempo de build será omitido con una advertencia.

Paso 2: Eliminar NgModules innecesarios

Con todas las declaraciones ahora standalone, muchos NgModules se convierten en cáscaras vacías. Esta pasada identifica módulos que solo reexportaban declaraciones standalone y los elimina.

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

Seleccionar "Remove unnecessary NgModule classes". Los módulos que aún contienen providers, configuraciones de rutas, o son importados por múltiples otros módulos serán preservados con un comentario TODO para revisión manual.

Requisito de Angular 22

Angular 22 requiere TypeScript 6 y Node.js 26. Versiones anteriores no son soportadas. Ejecutar ng version para verificar compatibilidad antes de comenzar la migración.

Paso 3: Cambiar al bootstrap standalone

La pasada final reemplaza el NgModule raíz con la API bootstrapApplication de Angular y convierte el componente raíz a 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]))
  ]
};

El patrón ApplicationConfig reemplaza los arrays providers e imports del módulo raíz. Todas las funciones provider (provideRouter, provideHttpClient, provideAnimations) funcionan directamente sin wrappers de módulos.

Angular 22: OnPush por defecto y componentes sin selector

Angular 22 introdujo dos cambios que afectan el desarrollo de componentes standalone: OnPush como estrategia de detección de cambios por defecto y los componentes sin selector alcanzando estado estable.

OnPush por defecto

Los nuevos componentes generados en Angular 22 usan ChangeDetectionStrategy.OnPush por defecto. Esto fomenta la reactividad basada en signals sobre las verificaciones disparadas por Zone.js. Durante la migración, el schematic agrega explícitamente changeDetection: ChangeDetectionStrategy.Default a componentes existentes para mantener compatibilidad hacia atrás.

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

Para aplicaciones migradas, la adopción de OnPush de forma incremental se logra reemplazando propiedades de clase con signals. Este enfoque se alinea con la arquitectura signal-first que promueve Angular 22.

Componentes sin selector (estable)

Los componentes sin selector permiten importar componentes directamente en templates sin definir selectores de texto. Esto elimina conflictos de nombres de selectores en bases de código grandes y proporciona mejor seguridad de tipos.

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

En templates, los componentes sin selector se usan por su nombre de clase en lugar de una etiqueta de selector personalizada. El compilador asegura que el import existe, detectando errores en tiempo de build en lugar de runtime.

Migración de rutas: de módulos a loadComponent

Los módulos de rutas requieren atención manual porque el schematic no convierte automáticamente los imports de módulos loadChildren a loadComponent o loadChildren a nivel de ruta con rutas standalone.

El patrón legacy cargaba módulos de características completos:

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

El equivalente standalone carga componentes individuales o archivos de rutas directamente:

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 carga un solo componente con lazy loading. loadChildren con un archivo de rutas carga un área de características completa con lazy loading. Ambos producen chunks separados que el navegador obtiene bajo demanda.

¿Listo para aprobar tus entrevistas de Angular?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Manejo de SharedModules y dependencias comunes

Los SharedModules, esos módulos catch-all que exportan componentes, directivas y pipes comúnmente usados, son el bloqueador más común durante la migración. El schematic no puede eliminarlos automáticamente porque múltiples módulos los importan.

La solución: convertir declaraciones compartidas a standalone individualmente, luego eliminar el SharedModule una vez que nada lo 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 {}

Los consumidores ahora importan LoadingSpinner directamente en lugar del SharedModule completo. El bundler incluye solo los componentes específicos que cada ruta necesita.

Imponer desarrollo exclusivamente standalone

Después de la migración, prevenir que nuevos NgModules vuelvan a aparecer en el código base es esencial. Angular proporciona una opción del compilador TypeScript para esto.

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

Con strictStandalone habilitado, cualquier intento de crear un componente, directiva o pipe no standalone produce un error de compilación. Esto impone la nueva arquitectura en todo el equipo. La migración de formularios Angular sigue el mismo patrón con Signal Forms ahora estables en Angular 22.

Ganancias de rendimiento: tamaño de bundle y lazy loading

El principal beneficio de rendimiento de los componentes standalone proviene del lazy loading granular. Con NgModules, el lazy loading operaba a nivel de módulo: importar un componente de un módulo traía todas las declaraciones que ese módulo exportaba. Los componentes standalone rompen este acoplamiento.

Un benchmark real en una aplicación empresarial mediana (200+ componentes) midió antes y después de la migración standalone:

MétricaBasado en NgModuleStandaloneMejora
Bundle inicial485 KB218 KB-55%
Chunk lazy más grande142 KB38 KB-73%
Time to Interactive3.2s1.8s-44%
Tiempo de build (esbuild)12.4s8.1s-35%

Estos números resultan de eliminar el overhead de resolución de módulos y habilitar al bundler para eliminar exports no usados a nivel de componente en lugar de nivel de módulo.

Pruebas de componentes standalone con Vitest

Las pruebas unitarias se simplifican significativamente con componentes standalone. Angular 22 estableció Vitest como el test runner por defecto, reemplazando Karma. La configuración de TestBed ya no requiere importar módulos completos para satisfacer las dependencias de un componente.

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

El componente va directamente en el array imports de TestBed.configureTestingModule. Todas sus dependencias declaradas ya están resueltas a través de sus propios imports, no se necesitan importaciones de módulos adicionales.

Errores comunes en la migración

Imports circulares entre componentes standalone. Cuando el componente A importa el componente B y B importa A, el compilador TypeScript genera un error de dependencia circular. La solución: extraer la interfaz compartida a un archivo separado o usar forwardRef() como solución temporal mientras se refactoriza la cadena de dependencias.

Bibliotecas de terceros aún usando NgModules. Muchas bibliotecas han migrado a standalone, pero algunos paquetes legacy todavía exportan NgModules. Importar estos módulos directamente en el array imports del componente standalone funciona, Angular soporta mezclar imports standalone y basados en módulos.

Providers faltantes después de eliminar AppModule. Los servicios previamente proporcionados en el array providers del módulo raíz deben moverse a ApplicationConfig en app.config.ts o usar providedIn: 'root' en el decorador @Injectable. Los servicios con scope de ruta deben usar el array providers en configuraciones de ruta.

Pregunta de entrevista

Cuando se pregunte sobre componentes standalone en una entrevista Angular, enfocarse en tres puntos: (1) la gestión de dependencias pasa del nivel de módulo al nivel de componente, (2) el tree-shaking se vuelve granular, y (3) la configuración de pruebas se simplifica porque los componentes declaran sus propios imports.

Fuentes

Migración a standalone: puntos clave para Angular 22

  • El schematic oficial del CLI de Angular automatiza 80-90% de la migración a través de tres pasadas secuenciales: convertir declaraciones, eliminar módulos, cambiar bootstrap
  • Los módulos de rutas necesitan conversión manual de loadChildren con NgModules a loadComponent o archivos de rutas standalone
  • Los SharedModules son el principal bloqueador, convertir cada declaración compartida a standalone individualmente, luego eliminar el módulo
  • Habilitar strictStandalone en tsconfig para prevenir que se introduzcan nuevos NgModules post-migración
  • Los tamaños de bundle se reducen 30-55% gracias al tree-shaking a nivel de componente y lazy loading granular con loadComponent
  • Las pruebas unitarias se simplifican con Vitest como runner por defecto, importar el componente standalone directamente en TestBed sin configuración de módulo
  • OnPush por defecto y componentes sin selector de Angular 22 combinan naturalmente con arquitectura standalone para una base de código más limpia y mantenible

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Reto diario

¿Sabrías detectar el bug en Angular?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 21 de agosto de 2026

Etiquetas

#angular
#standalone-components
#migration
#tutorial

Compartir

Artículos relacionados