Frontend

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

Guía de Angular Signals que Transformará tu Programación Reactiva

¡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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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:

TYPESCRIPT
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:

TYPESCRIPT
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:

JAVASCRIPT
// 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:

JAVASCRIPT
// 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:

JAVASCRIPT
// 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:

JAVASCRIPT
// 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 API

4. Patrón de Máquina de Estados con Signals

Construye máquinas de estados robustas con signals:

JAVASCRIPT
// 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

JAVASCRIPT
// ❌ 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

JAVASCRIPT
// ❌ 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

TSX
// ❌ 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

JAVASCRIPT
// ❌ 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 template

Signals 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! 🚀

Recursos