En esta página

Signals: primitivas reactivas en Angular

15 min lectura TextoCap. 2 — Reactividad

Qué son los Signals?

Los Signals son la primitiva reactiva central de Angular 21. Son contenedores de valores que notifican automaticamente a sus consumidores cuando cambian. Reemplazan progresivamente el sistema basado en Zone.js.

Crear un signal

Usa signal(valorInicial) para crear un signal mutable:

import { signal } from '@angular/core';

// Signal con tipo inferido (number)
const contador = signal(0);

// Signal con tipo explícito
const nombre = signal<string | null>(null);

// Signal con valor complejo
const usuario = signal({ nombre: 'Ana', edad: 28 });

Leer un signal

Los signals son funciones. Llama al signal para leer su valor:

console.log(contador());  // 0
console.log(nombre());    // null

En templates, también se llaman como funciones:

<p>Contador: {{ contador() }}</p>

Escribir en un signal

Hay dos formas de actualizar un signal:

`.set(nuevoValor)` — Reemplazo completo

contador.set(10);
nombre.set('Carlos');

`.update(fn)` — Basado en el valor anterior

contador.update(valor => valor + 1);
nombre.update(n => n ? n.toUpperCase() : 'ANON');

Para colecciones, siempre crea un nuevo array u objeto:

const items = signal<string[]>(['a', 'b']);

// Agregar elemento
items.update(lista => [...lista, 'c']);

// Eliminar elemento
items.update(lista => lista.filter(item => item !== 'b'));

// Modificar elemento
items.update(lista =>
  lista.map(item => item === 'a' ? 'A' : item)
);

Signals computados

computed() crea un signal de solo lectura que se deriva de otros signals:

import { signal, computed } from '@angular/core';

const precio = signal(100);
const cantidad = signal(3);
const impuesto = signal(0.16);

const subtotal = computed(() => precio() * cantidad());
const total = computed(() => subtotal() * (1 + impuesto()));

Caracteristicas de computed():

  • Solo lectura — No puedes usar .set() ni .update()
  • Perezoso — Solo se recalcula cuando se lee
  • Memoizado — Cachea el resultado hasta que las dependencias cambien
  • Auto-tracking — Las dependencias se detectan automaticamente

Signal vs Observable

Caracteristica Signal Observable
Lectura Sincrona: signal() Asincrona: subscribe()
Valor actual Siempre disponible No garantizado
Reactividad Automatica en templates Requiere async pipe
Composicion computed() pipe() + operadores
Caso de uso Estado de UI Eventos, HTTP, streams

Buenas prácticas

  1. Usa readonly en las declaraciones para evitar reasignacion
  2. Prefiere computed() sobre effects para derivar estado
  3. Mantien los signals granulares — Un signal por concepto
  4. Nunca mutes el valor interno — Siempre crea nuevas referencias

Práctica

  1. Crea un contador con signals: Implementa un componente con un signal(0) y botones para incrementar, decrementar y resetear. Muestra el valor en el template llamando al signal como funcion.
  2. Agrega un computed: Crea un computed() que derive si el contador es par o impar y otro que calcule el doble del valor. Muestralos en el template.
  3. Gestiona una lista: Crea un signal con un array de strings. Implementa metodos para agregar y eliminar elementos usando .update() con spread operator, sin mutar el array original.

En la siguiente leccion aprenderemos sobre effects y la API resource para manejar efectos secundarios.

Nunca mutes directamente
Nunca hagas miSignal().push(item) ni modifiques propiedades del objeto interno. Siempre usa .set() o .update() para crear un nuevo valor. Los signals detectan cambios por referencia.
Computed es perezoso
Un computed() solo se recalcula cuando se lee Y alguna de sus dependencias cambio. Angular rastrea las dependencias automaticamente; no necesitas declararlas.
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';

interface Tarea {
  id: number;
  texto: string;
  completada: boolean;
  prioridad: 'alta' | 'media' | 'baja';
}

@Component({
  selector: 'app-gestor-tareas',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './gestor-tareas.html',
})
export class GestorTareas {
  readonly tareas = signal<Tarea[]>([
    { id: 1, texto: 'Aprender signals', completada: false, prioridad: 'alta' },
    { id: 2, texto: 'Crear componentes', completada: true, prioridad: 'media' },
    { id: 3, texto: 'Configurar routing', completada: false, prioridad: 'baja' },
  ]);

  readonly filtroPrioridad = signal<string>('todas');

  readonly tareasFiltradas = computed(() => {
    const filtro = this.filtroPrioridad();
    const lista = this.tareas();
    if (filtro === 'todas') return lista;
    return lista.filter(t => t.prioridad === filtro);
  });

  readonly estadisticas = computed(() => {
    const lista = this.tareas();
    return {
      total: lista.length,
      completadas: lista.filter(t => t.completada).length,
      pendientes: lista.filter(t => !t.completada).length,
      porcentaje: lista.length
        ? Math.round((lista.filter(t => t.completada).length / lista.length) * 100)
        : 0,
    };
  });

  agregarTarea(texto: string, prioridad: 'alta' | 'media' | 'baja'): void {
    this.tareas.update(lista => [
      ...lista,
      { id: Date.now(), texto, completada: false, prioridad },
    ]);
  }

  toggleTarea(id: number): void {
    this.tareas.update(lista =>
      lista.map(t => t.id === id ? { ...t, completada: !t.completada } : t)
    );
  }

  eliminarTarea(id: number): void {
    this.tareas.update(lista => lista.filter(t => t.id !== id));
  }
}