🇺🇸 EN
Angular
·6 min read·↗·🇺🇸 EN

Cómo empezar con Standalone Components en Angular (Guía moderna completa)

Portada principal del artículo: Cómo empezar con Standalone Components en Angular (Guía moderna completa)

Cuando Angular introdujo por primera vez los Standalone Components, marcó el inicio de un cambio arquitectónico clave para dejar atrás la complejidad del sistema NgModule. Hoy en día, los Standalone Components son el estándar oficial por defecto en Angular (v17, v18 y v19+).

Al eliminar las declaraciones en NgModule, los componentes standalone simplifican la gestión de dependencias, aceleran la compilación, facilitan la carga perezosa (lazy loading) y hacen que la incorporación de nuevos desarrolladores al equipo sea mucho más rápida.

⚡ ¿Usas Angular moderno (v17+)? Salta directamente a Arquitectura moderna con Standalone: app.config.ts, Signals y Vitest para ver la estructura estándar más reciente de un proyecto.

En este artículo te muestro cómo crear, importar, configurar rutas y escribir pruebas unitarias para Standalone Components en Angular moderno.

Empecemos generando nuestro primer Standalone Component.


1. Crear un Standalone Component 🧩

En las versiones modernas del Angular CLI (ng new), todos los componentes son standalone por defecto.

Si generas un componente mediante el CLI:

ng generate component components/user-card

El componente generado declara sus propias dependencias dentro de los metadatos de @Component:

// user-card.component.ts
import { Component, input, output } from '@angular/core';
 
export interface User {
  id: string;
  name: string;
  email: string;
}
 
@Component({
  selector: 'app-user-card',
  standalone: true, // 👈 Por defecto en Angular moderno
  imports: [],      // 👈 Declara directamente otros componentes, directivas o pipes
  template: `
    <div class="user-card p-4 border rounded-xl shadow-xs">
      <h3 class="font-bold">{{ user().name }}</h3>
      <p class="text-neutral-500">{{ user().email }}</p>
      <button (click)="remove.emit(user().id)" class="btn-danger mt-2">
        Remove
      </button>
    </div>
  `
})
export class UserCardComponent {
  readonly user = input.required<User>();
  readonly remove = output<string>();
}

Ahora veamos cómo usar un Standalone Component dentro de otro.


2. Importar Standalone Components en otros componentes 🔗

Cuando un componente padre necesita renderizar un componente hijo standalone, agregas el hijo directamente en el arreglo imports: [...] del padre:

// user-list.component.ts
import { Component, signal } from '@angular/core';
import { UserCardComponent, User } from '../user-card/user-card.component';
 
@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [UserCardComponent], // 👈 Declaración directa de dependencias
  template: `
    <section class="user-list space-y-4">
      <h2>Active Team Members</h2>
 
      @for (member of members(); track member.id) {
        <app-user-card [user]="member" (remove)="onRemoveMember($event)" />
      } @empty {
        <p class="text-neutral-500">No members found.</p>
      }
    </section>
  `
})
export class UserListComponent {
  readonly members = signal<User[]>([
    { id: '1', name: 'Dany Paredes', email: 'dany@example.com' },
    { id: '2', name: 'Alex Johnson', email: 'alex@example.com' }
  ]);
 
  onRemoveMember(id: string) {
    this.members.update(list => list.filter(u => u.id !== id));
  }
}

Nota que no necesitas CommonModule solo para bucles o condicionales: el nuevo flujo de control integrado de Angular (@for e @if) funciona de forma nativa en cualquier lugar.

Ahora revisemos cómo funcionan el enrutamiento y la carga perezosa con Standalone Components.


3. Configuración de rutas y Lazy Loading con loadComponent 🚀

En el enfoque tradicional basado en módulos, la carga perezosa requería un archivo NgModule independiente junto con loadChildren. Con Standalone Components, puedes cargar componentes individuales bajo demanda de forma directa usando loadComponent:

// app.routes.ts
import { Routes } from '@angular/router';
 
export const routes: Routes = [
  {
    path: '',
    redirectTo: 'users',
    pathMatch: 'full'
  },
  {
    path: 'users',
    // Carga perezosa del componente standalone bajo demanda
    loadComponent: () => import('./components/user-list/user-list.component')
      .then(m => m.UserListComponent)
  },
  {
    path: 'settings',
    loadComponent: () => import('./pages/settings/settings.component')
      .then(m => m.SettingsComponent)
  }
];

El empaquetador de Angular generará automáticamente un fragmento (chunk) de JavaScript dedicado para cada ruta.

Ahora veamos cómo se inicia toda la aplicación sin necesidad de un AppModule.


4. Arquitectura moderna con Standalone: app.config.ts, Signals y Vitest ⚡

En Angular moderno, AppModule ha desaparecido por completo. Las aplicaciones se inician usando bootstrapApplication en main.ts, configuradas a través de app.config.ts:

app.config.ts: Providers de la aplicación

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
 
export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withFetch())
  ]
};

main.ts: Inicialización limpia

// main.ts
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));

Ahora veamos lo sencillo que resulta escribir pruebas unitarias para Standalone Components usando Vitest.


5. Pruebas unitarias de Standalone Components con Vitest 🧪

Como los Standalone Components declaran sus propias dependencias, escribir pruebas unitarias en Vitest es rápido y no requiere configurar módulos intermedios:

// user-card.component.spec.ts (Vitest)
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent, User } from './user-card.component';
import { ComponentRef } from '@angular/core';
 
describe('UserCardComponent with Vitest', () => {
  let component: UserCardComponent;
  let componentRef: ComponentRef<UserCardComponent>;
  let fixture: ComponentFixture<UserCardComponent>;
 
  const mockUser: User = { id: '101', name: 'Dany Paredes', email: 'dany@example.com' };
 
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserCardComponent] // 👈 Importa el componente standalone directamente
    }).compileComponents();
 
    fixture = TestBed.createComponent(UserCardComponent);
    component = fixture.componentInstance;
    componentRef = fixture.componentRef;
 
    // Asignar el valor del signal input
    componentRef.setInput('user', mockUser);
    fixture.detectChanges();
  });
 
  it('should render user information correctly', () => {
    const el = fixture.nativeElement as HTMLElement;
    expect(el.querySelector('h3')?.textContent).toBe('Dany Paredes');
    expect(el.querySelector('p')?.textContent).toBe('dany@example.com');
  });
 
  it('should emit remove event with user ID when clicked', () => {
    let emittedId = '';
    component.remove.subscribe((id) => (emittedId = id));
 
    const button = fixture.nativeElement.querySelector('button');
    button.click();
 
    expect(emittedId).toBe('101');
  });
});

Comparativa: NgModule tradicional vs Arquitectura Standalone moderna 📊

AspectoAngular tradicional (v2 a v16)Angular moderno (v17+)
Registro de componentesDebe declararse en un @NgModulestandalone: true (por defecto)
Importación de dependenciasSe inyectan mediante NgModule.importsSe declaran directamente en @Component.imports
Inicio de la aplicaciónplatformBrowserDynamic().bootstrapModule(AppModule)bootstrapApplication(AppComponent, appConfig)
Carga perezosa (Lazy Loading)loadChildren: () => import(...).then(m => m.MyModule)loadComponent: () => import(...).then(m => m.MyComponent)
Pruebas unitariasTestBed pesado con módulos de soporteTestBed ligero o Vitest directo

Resumen 🛠️

Los Standalone Components hacen que Angular sea más limpio y directo de trabajar:

  1. Los componentes, directivas y pipes declaran sus propias dependencias directamente a través de imports: [...].
  2. Usas bootstrapApplication y app.config.ts para configurar los providers de forma limpia y centralizada.
  3. Usas loadComponent para una carga perezosa inmediata a nivel de rutas, sin módulos intermedios.
  4. Pruebas tus Standalone Components de forma ágil y directa usando Vitest.

Si quieres seguir aprendiendo sobre Angular moderno, echa un vistazo a mis artículos sobre Cómo usar ng-template, ng-container y ng-content y Cómo usar parámetros de ruta con Signals.

Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

Preguntas Frecuentes

¿Qué son los standalone components en Angular y en qué se diferencian de los componentes basados en módulos?

Los componentes standalone no pertenecen a ningún NgModule. En su lugar, declaran sus dependencias directamente en el arreglo imports del propio componente. En Angular moderno (v17+), standalone es la opción por defecto para todos los componentes, directivas y pipes nuevos.

¿Cómo inicializar una aplicación de Angular sin un AppModule?

En main.ts, llama a 'bootstrapApplication(AppComponent, appConfig)' donde appConfig contiene los providers globales configurados con provideRouter(routes), provideHttpClient() y otros providers funcionales.

¿Cómo funciona el lazy loading con componentes standalone?

En lugar de usar loadChildren con un NgModule, usa 'loadComponent: () => import('./path/to/component').then(m => m.MyComponent)' directamente en la configuración de tus rutas.

¿Cómo probar componentes standalone con Vitest en Angular?

En tu archivo de pruebas de Vitest, importa el componente standalone directamente en TestBed.configureTestingModule({ imports: [MyStandaloneComponent] }) o usa MockBuilder de ng-mocks, sin necesidad de configurar un módulo intermedio.

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.