En esta página
Signals: primitivas reactivas en Angular
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()); // nullEn 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
- Usa
readonlyen las declaraciones para evitar reasignacion - Prefiere
computed()sobre effects para derivar estado - Mantien los signals granulares — Un signal por concepto
- Nunca mutes el valor interno — Siempre crea nuevas referencias
Práctica
- 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. - 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. - 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));
}
}
Inicia sesión para guardar tu progreso