Angular 21 consolida el modelo de componentes standalone como el estándar absoluto del framework. Si vienes de versiones anteriores que usaban NgModules, o si estás empezando con Angular ahora mismo, esta guía te explica todo lo que necesitas saber sobre cómo funciona la arquitectura moderna de Angular.
¿Qué son los componentes standalone?
Los componentes standalone son componentes que no dependen de un NgModule para funcionar. Se gestionan a sí mismos: declaran sus propias dependencias directamente. En Angular 21, todos los proyectos nuevos usan standalone por defecto y NgModules ha quedado como una forma legacy.
Crear un proyecto Angular 21
# Instalar Angular CLI globalmente
npm install -g @angular/cli
# Crear nuevo proyecto (standalone por defecto)
ng new mi-proyecto --standalone
# Entrar al directorio e iniciar el servidor
cd mi-proyecto
ng serve
Anatomía de un componente standalone
import { Component, input, output, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-tarjeta-producto',
standalone: true,
imports: [CommonModule, RouterLink], // Dependencias del componente
template: `
<div class="tarjeta" [class.destacada]="producto().destacado">
<h3>{{ producto().nombre }}</h3>
<p>{{ producto().descripcion }}</p>
<span class="precio">{{ producto().precio | currency:'EUR' }}</span>
<button (click)="agregarAlCarrito()">
Añadir al carrito
</button>
<a [routerLink]="['/producto', producto().id]">Ver más</a>
</div>
`,
})
export class TarjetaProductoComponent {
// Input como signal (Angular 17+)
producto = input.required<Producto>();
// Output moderno
carritoActualizado = output<Producto>();
// Estado local con signals
añadido = signal(false);
agregarAlCarrito() {
this.añadido.set(true);
this.carritoActualizado.emit(this.producto());
}
}
Signals: la revolución del estado en Angular
Angular Signals, introducidos en Angular 16 y maduros en Angular 21, son la forma moderna de gestionar el estado reactivo. Reemplazan gradualmente a RxJS para casos de uso de estado local y hacen que la detección de cambios sea mucho más eficiente.
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-contador',
standalone: true,
template: `
<p>Conteo: {{ count() }}</p>
<p>Doble: {{ doble() }}</p>
<button (click)="incrementar()">+</button>
<button (click)="decrementar()">-</button>
`
})
export class ContadorComponent {
count = signal(0);
// computed: se actualiza automáticamente cuando count cambia
doble = computed(() => this.count() * 2);
constructor() {
// effect: ejecuta código cuando las signals cambian
effect(() => {
console.log('El conteo cambió a:', this.count());
});
}
incrementar() { this.count.update(v => v + 1); }
decrementar() { this.count.update(v => v - 1); }
}
Inyección de dependencias sin constructor
Angular 21 permite inyectar servicios de forma más limpia usando la función inject(), sin necesidad de declararlos en el constructor:
import { Component, inject, signal, OnInit } from '@angular/core';
import { ProductoService } from './producto.service';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-lista-productos',
standalone: true,
template: `
@if (cargando()) {
<p>Cargando productos...</p>
} @else {
@for (producto of productos(); track producto.id) {
<app-tarjeta-producto [producto]="producto" />
} @empty {
<p>No hay productos disponibles.</p>
}
}
`
})
export class ListaProductosComponent implements OnInit {
private productoService = inject(ProductoService);
cargando = signal(true);
productos = signal<Producto[]>([]);
ngOnInit() {
this.productoService.obtenerTodos().subscribe({
next: (data) => {
this.productos.set(data);
this.cargando.set(false);
},
error: () => this.cargando.set(false)
});
}
}
Nueva sintaxis de plantillas: @if, @for, @switch
Angular 17+ introdujo una nueva sintaxis de flujo de control que reemplaza a las directivas *ngIf, *ngFor y *ngSwitch. Es más legible y tiene mejor rendimiento.
<!-- Antes (ngIf) -->
<div *ngIf="usuario; else sinUsuario">
<p>Hola, {{ usuario.nombre }}</p>
</div>
<ng-template #sinUsuario>
<p>No hay usuario</p>
</ng-template>
<!-- Ahora (@if) - mucho más claro -->
@if (usuario()) {
<p>Hola, {{ usuario()!.nombre }}</p>
} @else {
<p>No hay usuario</p>
}
<!-- Antes (ngFor) -->
<li *ngFor="let item of items; trackBy: trackById">{{ item.nombre }}</li>
<!-- Ahora (@for) - con track obligatorio y @empty incluido -->
@for (item of items(); track item.id) {
<li>{{ item.nombre }}</li>
} @empty {
<li>No hay elementos</li>
}
Routing con componentes standalone
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/home/home.component').then(m => m.HomeComponent)
},
{
path: 'productos',
loadComponent: () =>
import('./pages/productos/productos.component').then(m => m.ProductosComponent)
},
{
path: 'producto/:id',
loadComponent: () =>
import('./pages/detalle/detalle.component').then(m => m.DetalleComponent)
},
{
path: '**',
loadComponent: () =>
import('./pages/not-found/not-found.component').then(m => m.NotFoundComponent)
}
];
// main.ts - Bootstrap sin módulos
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
]
});
Angular 21 es más limpio, más rápido y más fácil de aprender que nunca. Los componentes standalone, los signals y la nueva sintaxis de plantillas hacen que el código sea más expresivo y mantenible. Si aún no has migrado tu forma de pensar Angular al modelo moderno, ahora es el momento.