Guía de Angular Signals que Transformará tu Programación Reactiva
Finalmente, un primitivo reactivo que hace la gestión de estado intuitiva y performante sin la complejidad de RxJS

¡Hola, developers! 👋
¿Recuerdas la última vez que debugueaste una cadena compleja de RxJS con múltiples suscripciones, async pipes por todos lados, y ese memory leak que no podías rastrear? ¿O cuando tuviste que explicarle a un desarrollador junior por qué necesita hacer unsubscribe de los observables??
Hoy, quiero compartir Angular Signals - un nuevo primitivo reactivo que cambia fundamentalmente cómo manejamos el estado en aplicaciones Angular. Al final de este artículo, entenderás cómo aprovechar signals para tener programación reactiva más limpia y performante sin la sobrecarga tradicional de RxJS.
¿Qué son los Angular Signals?
Piensa en signals como "variables inteligentes" que automáticamente rastrean cuándo son leídas y notifican cuando cambian. Son como un rastreador GPS para tus datos - siempre sabiendo quién está observando y actualizando eficientemente solo lo que necesita cambiar.
Angular Signals resuelven el problema fundamental de la reactividad granular: saber exactamente qué cambió y actualizar solo las partes afectadas de tu UI, sin gestión manual de suscripciones o preocupaciones con change detection.
¿Cuándo Deberías Usar Angular Signals?
Buenos casos de uso:
- Gestión de estado de componentes que necesita actualizaciones reactivas
- Valores computados derivados de otras fuentes reactivas
- Estado y lógica de validación de formularios
- Estado compartido entre componentes sin servicios
- Actualizaciones de UI críticas para performance con change detection mínimo
Cuándo NO usar Signals:
- Peticiones HTTP y operaciones asíncronas (continúa con Observables)
- Streams de eventos complejos que necesitan operators como debounce, throttle
- Integración con codebases pesadas en RxJS (usa interop con cuidado)
Signals: Tu Primera Implementación
Construyamos un ejemplo práctico: un contador de productos con cálculo de precio en tiempo real que demuestra los conceptos principales de signals.
Paso 1: Creando Tu Primer Signal
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-producto',
template: `
<div>
<h2>Producto: {{ nombreProducto() }}</h2>
<p>Cantidad: {{ cantidad() }}</p>
<button (click)="incrementar()">Agregar al Carrito</button>
</div>
`
})
export class ProductoComponent {
// Creando signals escribibles
nombreProducto = signal('Libro Angular');
cantidad = signal(0);
incrementar() {
// Actualizando valor del signal
this.cantidad.set(this.cantidad() + 1);
}
}Este código crea dos signals - nota cómo los llamamos como funciones en el template. Los signals son funciones que retornan su valor actual cuando son llamadas.
Paso 2: Trabajando con Computed Signals
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-producto',
template: `
<div>
<p>Cantidad: {{ cantidad() }}</p>
<p>Precio unitario: ${{ precioUnitario() }}</p>
<p>Total: ${{ precioTotal() }}</p>
<button (click)="incrementar()">Agregar Item</button>
</div>
`
})
export class ProductoComponent {
cantidad = signal(1);
precioUnitario = signal(29.99);
// Computed signal actualiza automáticamente cuando las dependencias cambian
precioTotal = computed(() => {
return this.cantidad() * this.precioUnitario();
});
incrementar() {
this.cantidad.update(c => c + 1);
}
}Los computed signals recalculan automáticamente cuando sus dependencias cambian. Sin suscripciones, sin actualizaciones manuales - simplemente funciona.
Paso 3: Signal Effects para Side Effects
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-producto'
})
export class ProductoComponent {
cantidad = signal(0);
inventario = signal(10);
constructor() {
// Effect ejecuta cuando los signals que lee cambian
effect(() => {
if (this.cantidad() > this.inventario()) {
console.log('¡Advertencia: La cantidad excede el inventario!');
this.mostrarAdvertenciaInventario = true;
}
});
}
agregarAlCarrito() {
if (this.cantidad() < this.inventario()) {
this.cantidad.update(c => c + 1);
}
}
}Los effects rastrean automáticamente las dependencias de signals y se re-ejecutan cuando esos signals cambian - perfecto para logging, analytics, o manipulaciones del DOM.
Un Ejemplo Más Complejo: Carrito de Compras con Filtros
Construyamos algo más realista - un carrito de compras con filtrado y cálculos en tiempo real:
import { Component, signal, computed } from '@angular/core';
interface Producto {
id: number;
nombre: string;
precio: number;
categoria: string;
enStock: boolean;
}
@Component({
selector: 'app-carrito-compras',
template: `
<div class="carrito">
<input
placeholder="Buscar productos..."
(input)="terminoBusqueda.set($event.target.value)"
/>
<select (change)="categoriaSeleccionada.set($event.target.value)">
<option value="todas">Todas las Categorías</option>
<option *ngFor="let cat of categorias()" [value]="cat">
{{ cat }}
</option>
</select>
<div class="productos">
<div *ngFor="let producto of productosFiltrados()">
<h3>{{ producto.nombre }}</h3>
<p>${{ producto.precio }}</p>
<button
(click)="agregarAlCarrito(producto)"
[disabled]="!producto.enStock"
>
Agregar al Carrito
</button>
</div>
</div>
<div class="resumen">
<p>Items en el carrito: {{ itemsCarrito().length }}</p>
<p>Total: ${{ totalCarrito() }}</p>
<p>Con impuesto (10%): ${{ totalConImpuesto() }}</p>
</div>
</div>
`
})
export class CarritoComprasComponent {
// Signals de estado
productos = signal<Producto[]>([
{ id: 1, nombre: 'Portátil', precio: 999, categoria: 'Electrónica', enStock: true },
{ id: 2, nombre: 'Ratón', precio: 29, categoria: 'Electrónica', enStock: true },
{ id: 3, nombre: 'Escritorio', precio: 299, categoria: 'Muebles', enStock: false },
{ id: 4, nombre: 'Silla', precio: 199, categoria: 'Muebles', enStock: true }
]);
itemsCarrito = signal<Producto[]>([]);
terminoBusqueda = signal('');
categoriaSeleccionada = signal('todas');
// Computed signals para estado derivado
categorias = computed(() => {
const cats = new Set(this.productos().map(p => p.categoria));
return Array.from(cats);
});
productosFiltrados = computed(() => {
const termino = this.terminoBusqueda().toLowerCase();
const categoria = this.categoriaSeleccionada();
return this.productos().filter(producto => {
const coincideBusqueda = producto.nombre.toLowerCase().includes(termino);
const coincideCategoria = categoria === 'todas' || producto.categoria === categoria;
return coincideBusqueda && coincideCategoria;
});
});
totalCarrito = computed(() => {
return this.itemsCarrito().reduce((suma, item) => suma + item.precio, 0);
});
totalConImpuesto = computed(() => {
return this.totalCarrito() * 1.1; // 10% impuesto
});
agregarAlCarrito(producto: Producto) {
this.itemsCarrito.update(items => [...items, producto]);
}
}Este ejemplo muestra cómo los signals manejan elegantemente relaciones reactivas complejas sin gestión manual de suscripciones.
Patrón Avanzado: Validación de Formulario Basada en Signals
Construyamos algo aún más sofisticado - un formulario reactivo con validación en tiempo real usando signals:
import { Component, signal, computed, effect } from '@angular/core';
interface ErroresFormulario {
email?: string;
contrasena?: string;
confirmarContrasena?: string;
}
@Component({
selector: 'app-formulario-registro',
template: `
<form (submit)="enviarFormulario($event)">
<div>
<input
type="email"
placeholder="Email"
[value]="email()"
(input)="email.set($event.target.value)"
[class.error]="errores().email"
/>
<span class="msg-error">{{ errores().email }}</span>
</div>
<div>
<input
type="password"
placeholder="Contraseña"
[value]="contrasena()"
(input)="contrasena.set($event.target.value)"
[class.error]="errores().contrasena"
/>
<span class="msg-error">{{ errores().contrasena }}</span>
</div>
<div>
<input
type="password"
placeholder="Confirmar Contraseña"
[value]="confirmarContrasena()"
(input)="confirmarContrasena.set($event.target.value)"
[class.error]="errores().confirmarContrasena"
/>
<span class="msg-error">{{ errores().confirmarContrasena }}</span>
</div>
<button [disabled]="!formularioValido()">
Registrarse
</button>
<div class="medidor-fuerza">
Fuerza de la Contraseña: {{ fuerzaContrasena() }}
</div>
</form>
`
})
export class FormularioRegistroComponent {
// Signals de campos del formulario
email = signal('');
contrasena = signal('');
confirmarContrasena = signal('');
camposTocados = signal<Set<string>>(new Set());
// Reglas de validación como computed signals
errores = computed<ErroresFormulario>(() => {
const errores: ErroresFormulario = {};
const tocados = this.camposTocados();
// Validación de email
if (tocados.has('email')) {
const valorEmail = this.email();
if (!valorEmail) {
errores.email = 'El email es obligatorio';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valorEmail)) {
errores.email = 'Formato de email inválido';
}
}
// Validación de contraseña
if (tocados.has('contrasena')) {
const pwd = this.contrasena();
if (!pwd) {
errores.contrasena = 'La contraseña es obligatoria';
} else if (pwd.length < 8) {
errores.contrasena = 'La contraseña debe tener al menos 8 caracteres';
}
}
// Validación de confirmación de contraseña
if (tocados.has('confirmarContrasena')) {
if (this.contrasena() !== this.confirmarContrasena()) {
errores.confirmarContrasena = 'Las contraseñas no coinciden';
}
}
return errores;
});
fuerzaContrasena = computed(() => {
const pwd = this.contrasena();
if (pwd.length < 6) return 'Débil';
if (pwd.length < 10) return 'Media';
if (/[A-Z]/.test(pwd) && /[0-9]/.test(pwd) && /[^A-Za-z0-9]/.test(pwd)) {
return 'Fuerte';
}
return 'Media';
});
formularioValido = computed(() => {
return this.email() &&
this.contrasena() &&
this.confirmarContrasena() &&
Object.keys(this.errores()).length === 0;
});
constructor() {
// Auto-guardar borrador en localStorage
effect(() => {
const borrador = {
email: this.email(),
timestamp: Date.now()
};
localStorage.setItem('borradorRegistro', JSON.stringify(borrador));
});
}
marcarComoTocado(campo: string) {
this.camposTocados.update(campos => {
campos.add(campo);
return new Set(campos);
});
}
enviarFormulario(event: Event) {
event.preventDefault();
if (this.formularioValido()) {
console.log('Formulario enviado:', {
email: this.email(),
contrasena: this.contrasena()
});
}
}
}Esto demuestra cómo los signals pueden reemplazar librerías de formularios complejas con lógica de validación simple y reactiva.
Angular Signals con TypeScript
Para usuarios de TypeScript, aquí está cómo hacer tus implementaciones de signal type-safe:
// tipos.ts
interface Usuario {
id: number;
nombre: string;
email: string;
rol: 'admin' | 'usuario' | 'invitado';
}
interface EstadoApp {
usuarioActual: Usuario | null;
estaAutenticado: boolean;
permisos: string[];
}
// signal-store.service.ts
import { Injectable, signal, computed, Signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class SignalStore {
// Signals escribibles con tipos explícitos
private _usuarioActual = signal<Usuario | null>(null);
private _estaCargando = signal<boolean>(false);
// Computed signals de solo lectura
readonly usuarioActual: Signal<Usuario | null> = this._usuarioActual.asReadonly();
readonly estaAutenticado = computed<boolean>(() => !!this._usuarioActual());
readonly permisos = computed<string[]>(() => {
const usuario = this._usuarioActual();
if (!usuario) return [];
switch(usuario.rol) {
case 'admin': return ['leer', 'escribir', 'eliminar', 'admin'];
case 'usuario': return ['leer', 'escribir'];
case 'invitado': return ['leer'];
}
});
// Métodos de actualización type-safe
login(usuario: Usuario): void {
this._usuarioActual.set(usuario);
}
actualizarUsuario(actualizaciones: Partial<Usuario>): void {
this._usuarioActual.update(actual =>
actual ? { ...actual, ...actualizaciones } : null
);
}
}
// Uso con TypeScript
@Component({
selector: 'app-perfil',
template: `
<div *ngIf="store.usuarioActual() as usuario">
<h2>{{ usuario.nombre }}</h2>
<p>Rol: {{ usuario.rol }}</p>
<ul>
<li *ngFor="let perm of store.permisos()">
{{ perm }}
</li>
</ul>
</div>
`
})
export class PerfilComponent {
constructor(public store: SignalStore) {}
}Patrones Avanzados y Mejores Prácticas
1. Patrón de Composición de Signals
Crea signals de orden superior que combinan múltiples fuentes de signals:
// Componer múltiples signals en un único estado reactivo
function crearListaPaginada<T>(items: Signal<T[]>, tamañoPagina: number) {
const paginaActual = signal(0);
const totalPaginas = computed(() =>
Math.ceil(items().length / tamañoPagina)
);
const itemsPaginados = computed(() => {
const inicio = paginaActual() * tamañoPagina;
return items().slice(inicio, inicio + tamañoPagina);
});
return {
items: itemsPaginados,
paginaActual: paginaActual.asReadonly(),
totalPaginas,
siguientePagina: () => paginaActual.update(p => Math.min(p + 1, totalPaginas() - 1)),
paginaAnterior: () => paginaActual.update(p => Math.max(p - 1, 0))
};
}2. Patrón de Memoización de Signals
Optimiza cálculos costosos con signals memoizados:
// Memoizar operaciones costosas
function crearSignalMemoizado<T, R>(
fuente: Signal<T>,
calcular: (valor: T) => R,
igualdad?: (a: R, b: R) => boolean
) {
let ultimaEntrada: T | undefined;
let ultimaSalida: R | undefined;
return computed(() => {
const actual = fuente();
if (ultimaEntrada === actual && ultimaSalida !== undefined) {
return ultimaSalida;
}
ultimaEntrada = actual;
ultimaSalida = calcular(actual);
return ultimaSalida;
}, { equal: igualdad });
}3. Patrón de Debouncing de Signals
Implementa signals con debounce para búsqueda y manejo de inputs:
// Signal con debounce para inputs de búsqueda
function crearSignalConDebounce<T>(valorInicial: T, retraso: number) {
const inmediato = signal(valorInicial);
const conDebounce = signal(valorInicial);
let timeoutId: any;
const definir = (valor: T) => {
inmediato.set(valor);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
conDebounce.set(valor);
}, retraso);
};
return {
inmediato: inmediato.asReadonly(),
conDebounce: conDebounce.asReadonly(),
definir
};
}
// Uso
const busqueda = crearSignalConDebounce('', 300);
// busqueda.inmediato() - valor instantáneo
// busqueda.conDebounce() - valor con debounce para llamadas API4. Patrón de Máquina de Estados con Signals
Construye máquinas de estados robustas con signals:
// Máquina de estados usando signals
function crearMaquinaDeEstados<T extends string>(
estadoInicial: T,
transiciones: Record<T, T[]>
) {
const estadoActual = signal(estadoInicial);
const puedeTransicionarA = computed(() => {
return transiciones[estadoActual()] || [];
});
const transicionarA = (nuevoEstado: T) => {
if (puedeTransicionarA().includes(nuevoEstado)) {
estadoActual.set(nuevoEstado);
return true;
}
return false;
};
return {
estado: estadoActual.asReadonly(),
puedeTransicionarA,
transicionarA
};
}Errores Comunes a Evitar
1. Mutar Objetos Dentro de Signals
// ❌ No hagas esto - mutar objeto no dispara actualizaciones
const usuario = signal({ nombre: 'Juan', edad: 30 });
usuario().nombre = 'María'; // ¡Esto no disparará change detection!
// ✅ Haz esto en su lugar - crea nueva referencia de objeto
usuario.update(u => ({ ...u, nombre: 'María' }));
// O
usuario.set({ ...usuario(), nombre: 'María' });2. Crear Signals Dentro de Computed
// ❌ Ejemplo problemático - crea nuevo signal en cada cálculo
const computedMalo = computed(() => {
const signalTemp = signal(0); // ¡No crees signals aquí!
return signalTemp() + otroSignal();
});
// ✅ Solución - crea signals fuera de computed
const signalTemp = signal(0);
const computedBueno = computed(() => {
return signalTemp() + otroSignal();
});3. Olvidar Llamar Funciones Signal
// ❌ Evita este patrón - olvidando paréntesis
@Component({
template: `<div>{{ contador }}</div>` // ¡No se actualizará!
})
export class ComponenteMalo {
contador = signal(0);
}
// ✅ Enfoque preferido - siempre llama signals como funciones
@Component({
template: `<div>{{ contador() }}</div>` // Propiamente reactivo
})
export class ComponenteBueno {
contador = signal(0);
}Cuándo NO Usar Signals
No uses signals cuando:
- Trabajando con peticiones HTTP - Los Observables manejan mejor las operaciones asíncronas
- Necesites operators de stream complejos (debounce, throttle, retry) - RxJS es más poderoso
- Integrando con APIs existentes basadas en Observable - sobrecarga de conversión innecesaria
// ❌ Exageración para escenarios simples
const resultadoHttp = signal<Datos | null>(null);
this.http.get('/api/datos').subscribe(datos => {
resultadoHttp.set(datos); // Conversión innecesaria
});
// ✅ Solución simple es mejor
datos$ = this.http.get('/api/datos');
// Usa async pipe en el templateSignals vs RxJS Observables
Los Signals son geniales para:
- Gestión de estado síncrono
- Valores computados simples
- Actualizaciones de UI críticas para performance
- Reducir código boilerplate
Considera Observables cuando necesites:
- Operaciones asíncronas → Peticiones HTTP, streams WebSocket
- Operators complejos → debounceTime, switchMap, retry
- Streams de eventos → fromEvent, interval, timer
Conclusión
Angular Signals son una herramienta poderosa que puede simplificar dramáticamente la gestión de estado en tus aplicaciones. Traen reactividad granular, rastreo automático de dependencias, y mejor rendimiento a tus componentes Angular.
Puntos clave:
- Los signals son funciones que mantienen y rastrean valores reactivos
- Los computed signals derivan automáticamente estado de otros signals
- Los effects manejan side effects con rastreo automático de dependencias
- Los signals eliminan la gestión manual de suscripciones y memory leaks
La próxima vez que vayas a usar un Subject o BehaviorSubject para estado de componente, recuerda los signals. Tu código será más limpio, más performante, y más fácil de entender.
¿Ya has empezado a usar signals en tus proyectos Angular? ¿Qué patrones has descubierto? ¡Comparte tus experiencias en los comentarios!
Si esto te ayudó a mejorar tus habilidades en Angular, ¡sígueme para más patrones y mejores prácticas modernas de Angular! 🚀



