This content is only available in Spanish.
Also available in English.
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! 🚀



