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.tsinterface Usuario { id: number; nombre: string; email: string; rol: 'admin' | 'usuario' | 'invitado';}interface EstadoApp { usuarioActual: Usuario | null; estaAutenticado: boolean; permisos: string[];}// signal-store.service.tsimport { 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 reactivofunction 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 costosasfunction 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úsquedafunction 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 };}// Usoconst 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 signalsfunction 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 actualizacionesconst usuario = signal({ nombre: 'Juan', edad: 30 });usuario().nombre = 'María'; // ¡Esto no disparará change detection!// ✅ Haz esto en su lugar - crea nueva referencia de objetousuario.update(u => ({ ...u, nombre: 'María' }));// Ousuario.set({ ...usuario(), nombre: 'María' });2. Crear Signals Dentro de Computed
// ❌ Ejemplo problemático - crea nuevo signal en cada cálculoconst computedMalo = computed(() => { const signalTemp = signal(0); // ¡No crees signals aquí! return signalTemp() + otroSignal();});// ✅ Solución - crea signals fuera de computedconst 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 simplesconst resultadoHttp = signal<Datos | null>(null);this.http.get('/api/datos').subscribe(datos => { resultadoHttp.set(datos); // Conversión innecesaria});// ✅ Solución simple es mejordatos$ = 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! 🚀


