Front-end

Qué son los Angular Signals

· 11 min de lectura

Qué son los Angular Signals: signal, computed y effect explicados con ejemplos reales, la era signal-first de Angular 22 y cuándo NO conviene usarlos.

Si llevas un par de años con Angular, ya has notado que Signals no es una feature más: es el cambio de fondo más grande que ha tenido el framework desde que llegaron los componentes standalone. No es RxJS con otro nombre, y tampoco es una moda que se va a ir en dos releases. Desde Angular 17 es estable, y con Angular 22 el propio equipo cierra dos años de migración: signals pasa de "opción moderna" a ser el modelo por defecto del framework, con OnPush como estrategia de detección de cambios de serie.

En este post te cuento qué son, por qué Angular los necesitaba, y sobre todo cuándo tiene sentido usarlos y cuándo no.

El problema que resuelven

Antes de Signals, Angular detectaba cambios con Zone.js. La idea: Zone.js "parchea" APIs del navegador (setTimeout, addEventListener, fetch...) para enterarse de cuándo puede haber pasado algo, y cuando pasa, dispara un ciclo de ChangeDetection que recorre todo el árbol de componentes de arriba a abajo, comparando qué cambió.

Funciona, pero tiene tres problemas que se notan de verdad en producción:

  • Coste innecesario. En una app grande, un click en un botón puede acabar revisando cien componentes que no tienen nada que ver.

  • Difícil de razonar. "¿Por qué se ha vuelto a renderizar este componente?" es una pregunta que en Zone.js a veces solo se responde con el profiler.

  • Efectos colaterales raros. Al parchear APIs nativas, Zone.js interactúa mal con algunas librerías de terceros (WebSockets, algunos SDKs, ciertos polyfills), y esos bugs son de los que te queman media tarde.

Con Signals, el modelo cambia: el dato sabe quién lo está leyendo. Cuando cambia un signal, Angular avisa solo a lo que depende de ese signal en concreto, sin recorrer nada más. No es "una optimización de Zone.js": es un motor de reactividad distinto.

Las tres piezas que necesitas dominar primero

Todo lo demás (inputs, outputs, queries, resource...) se construye encima de tres funciones.

signal() — el valor que se puede leer y escribir

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

const contador = signal(0);

contador();            // se lee como función → 0
contador.set(5);       // reemplaza el valor
contador.update(v => v + 1); // deriva del valor anterior

Un signal se lee llamándolo como función. La primera vez que te pasa se siente raro; a la semana ya no te acuerdas de cómo era @Input() sin esto.

computed() — estado derivado, de solo lectura

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

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

const total = computed(() => precio() * cantidad());

total();          // 300
cantidad.set(5);
total();          // 500, se recalcula solo

computed() : si nadie lee el resultado tras un cambio, no recalcula nada. Y no tiene .set() a propósito — es de solo lectura por diseño. Si te encuentras queriendo escribir en un computed, la señal es que en realidad necesitas otro signal() normal, o linkedSignal() (lo ves más abajo).

effect() — para efectos secundarios, no para lógica de negocio

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

@Component({ /* ... */ })
export class MiComponente {
  contador = signal(0);

  constructor() {
    effect(() => {
      console.log(`Cambió a: ${this.contador()}`);
      localStorage.setItem('contador', String(this.contador()));
    });
  }
}

effect() se ejecuta cada vez que cambia algún signal que lee dentro. Vale para logging, sincronizar con localStorage, integrar una librería que no habla el idioma de Angular. No lo uses para derivar estado de tu app — ahí es computed(). Es el error principal que veo: gente escribiendo un effect() que actualiza otro signal() cuando lo que quería, sin saberlo, era un computed().

signal, computed y effect: lectura, derivación y efecto secundario

Cómo llegamos hasta aquí

Signals no salió terminado de fábrica. Fue madurando release a release:

Versión

Qué trajo

Angular 16

signal(), computed(), effect() en developer preview

Angular 17

Signals pasa a estable; llega el nuevo control de flujo (@if, @for) pensado para trabajar bien con signals

Angular 18

input(), output(), model() y las signal queries (viewChild, contentChild...) pasan a estable

Angular 19

Llegan linkedSignal() y resource() (async reactivo, en preview)

Angular 20

Zoneless (sin Zone.js) pasa a estable

Angular 21

Signal Forms llega en experimental; mejoras de sintaxis en templates (spread, @switch con fall-through, arrow functions)

Angular 22 (3 jun 2026)

resource() / rxResource() / httpResource() pasan a estable; Signal Forms pasa a estable; OnPush se convierte en la estrategia de detección de cambios por defecto (breaking change); llega debounced() en experimental

Línea de tiempo de Signals: de developer preview en Angular 16 a signal-first en Angular 22

Angular 22 es, según el propio equipo, el cierre de dos años de migración hacia signals: lo que empezó como preview en la 16 ahora es el camino por defecto para un proyecto nuevo. Si estás en una versión anterior a la 17, nada de esto va a funcionar igual en tu proyecto — y probablemente ya tienes motivos de sobra para actualizar.

El resto del ecosistema, en corto

No quiero convertir este post en un manual de referencia de cada API. Pero sí conviene que sepas que existe, para que no te sorprenda cuando lo veas en código ajeno:

  • input() / output() / model() reemplazan @Input(), @Output() con EventEmitter, y el patrón manual de two-way binding. Los inputs son de solo lectura desde dentro del componente, lo que evita mutaciones accidentales.

  • Signal queries (viewChild(), contentChild()) sustituyen @ViewChild + ngAfterViewInit(). El valor es un signal, así que lo puedes usar directamente en un computed().

  • linkedSignal() es para el caso "quiero un valor derivado de otro, pero que el usuario lo pueda sobrescribir y que se resetee cuando cambia la fuente". Ejemplo típico: un selector de talla que se resetea al cambiar de color, pero el usuario lo puede tocar mientras tanto.

  • resource() / rxResource() / httpResource() traen datos async a este mundo reactivo, con cancelación automática cuando cambian sus dependencias (el mismo comportamiento que te da switchMap en RxJS, pero sin escribirlo tú). Desde Angular 22 las tres son estables, y httpResource() es la opción recomendada por defecto para una petición REST simple: le pasas una función que devuelve la request, y si algún signal que usa dentro cambia, la petición se reinicia sola.

    import { httpResource } from '@angular/common/http';
    
    const idUsuario = signal(1);
    
    const usuario = httpResource<Usuario>(() => `/api/usuarios/${idUsuario()}`);
    // usuario.value(), usuario.isLoading(), usuario.error() — sin subscribe(), sin switchMap
    
  • debounced() (Angular 22, experimental) toma un signal y un tiempo de espera, y te devuelve un Resource — no un signal plano — porque un valor debounced tiene de verdad un estado de "cargando" mientras espera. Es el reemplazo directo del combo FormControl + debounceTime + distinctUntilChanged para un buscador simple.

  • Signal Forms (experimental en Angular 21, estable en Angular 22) es el stack de formularios construido sobre signals desde cero, con form(), validación con schemas y una Submission API para guardar de forma async. Da para un post entero aparte; aquí solo que sepas que existe si ves form() en código de otros.

¿Signals mata a RxJS?

No, y no lo trates como una guerra. En la práctica:

Usa Signals para...

Usa RxJS para...

Estado síncrono de UI (formularios, flags, contadores)

Eventos complejos que de verdad necesitan operadores (retry con backoff, combinar 3+ streams)

Valores derivados (computed)

WebSockets, streams infinitos, eventos de alta frecuencia

Peticiones REST simples (httpResource())

Integraciones que ya tienes hechas en RxJS y no vas a reescribir solo por moda

Búsqueda con debounce (debounced())

Ojo con esto: antes de Angular 22, debounceTime + switchMap era casi la única forma decente de hacer un buscador con debounce. Con httpResource() y debounced() estables, ese hueco de RxJS se ha reducido bastante — no porque RxJS sea peor, sino porque ya no hace falta para lo básico. Donde sigue ganando RxJS es en lo genuinamente complejo: reintentos con backoff, combinar varios streams con combineLatest/merge, o eventos de alta frecuencia como scroll o WebSockets.

Cuando necesitas cruzar de un mundo al otro, usa la interoperabilidad oficial en vez de reinventarla:

import { toSignal, toObservable } from '@angular/core/rxjs-interop';

// Observable → Signal
const usuario = toSignal(this.http.get<Usuario>('/api/usuario'), { initialValue: null });

// Signal → Observable
const busqueda = signal('');
const busqueda$ = toObservable(busqueda).pipe(debounceTime(300));

Cuándo SÍ usar Signals

  • Cualquier estado local de un componente: formularios, contadores, toggles, filtros de una tabla.

  • Valores derivados de otro estado (computed() en vez de recalcular en el template o en ngOnChanges).

  • Comunicación padre-hijo con input() / output() / model() en componentes nuevos.

  • Peticiones REST directas a un endpoint: empieza por httpResource() antes de montar HttpClient + subscribe() a mano.

  • Cuando quieras migrar a zoneless: signals es el requisito de base, y desde Angular 22 es el camino recomendado de serie (junto con OnPush como estrategia por defecto).

Cuándo NO conviene forzarlos

Meter signals "porque toca" también es deuda técnica, igual que pasaba con MCP y otros patrones nuevos que se ponen de moda. No los fuerces si:

  • Tu proyecto sigue en Angular 15 o anterior y no está en tus planes actualizar pronto — no vale la pena parchear medio proyecto para tener signals sueltos conviviendo con @Input() clásico sin un plan claro.

  • Tienes un flujo async con reintentos, cancelación y combinación de varios streams: eso sigue siendo terreno de RxJS, aunque el resultado final lo expongas como signal con toSignal().

  • Estás en medio de una migración grande y aún no tienes tests que cubran el comportamiento actual. Cambiar el modelo de reactividad sin red de seguridad es la forma más rápida de introducir un bug sutil de "esto ya no se actualiza".

  • Solo por "estar a la moda" en un componente que no tiene ningún problema de rendimiento ni de legibilidad. A veces @Input() de toda la vida sigue siendo la opción más simple, y simple gana.

Errores típicos al empezar

  1. Mutar en vez de reemplazar. miSignal().push(x) no dispara reactividad. Usa .update(arr => [...arr, x]).

  2. Usar effect() para derivar estado. Si estás escribiendo en un signal dentro de otro effect(), casi seguro querías un computed().

  3. Leer un signal input antes de que Angular lo inicialice, típicamente en el constructor. Muévelo a un computed() o a un effect().

  4. Mezclar Zone.js y zoneless a medias sin entender que, si activas provideZonelessChangeDetection(), cualquier mutación fuera de signals necesita marcarse manualmente con ChangeDetectorRef.

  5. Migrar todo el proyecto de golpe. Los schematics oficiales (ng generate @angular/core:signal-input-migration, output-migration, signal-queries-migration) ayudan mucho, pero migra por feature y revisa el diff, no lo apliques a ciegas en un monorepo grande.

  6. Actualizar a Angular 22 sin revisar la estrategia de detección de cambios. Desde esta versión, OnPush es el valor por defecto para componentes sin estrategia explícita — antes era Default (ahora renombrado Eager). ng update debería marcar automáticamente tus componentes existentes como Eager para no romper nada, pero en un proyecto grande con mucha detección manual conviene revisar el diff antes de mergear.

Preguntas frecuentes

¿Tengo que reescribir todo mi proyecto Angular para usar Signals?

No. Signals convive con @Input()/@Output() clásicos y con RxJS sin problema. Puedes introducirlos componente a componente, empezando por los que tengan más lógica de estado local.

¿Signals reemplaza a RxJS?

No del todo. Reemplaza buena parte del estado síncrono de UI. Para eventos async complejos (debounce, retry, combinar streams), RxJS sigue siendo la herramienta correcta.

¿Necesito zoneless para usar Signals?

No es obligatorio: puedes usar Signals con Zone.js activo sin ningún problema, y sigue siendo habitual en proyectos que llevan tiempo corriendo. Zoneless es estable desde Angular 20, y desde Angular 22 (con OnPush como estrategia por defecto) es el camino que Angular recomienda de serie para proyectos nuevos.

¿Desde qué versión de Angular puedo usar todo esto?

Signals estable desde Angular 17. input()/output()/model()/signal queries, estables desde Angular 18. linkedSignal() llegó en Angular 19. resource(), rxResource(), httpResource() y Signal Forms pasaron a estable en Angular 22 (junio 2026), que es también cuando OnPush se convirtió en la estrategia de detección de cambios por defecto. Si estás en una versión anterior, actualiza antes de intentar seguir este post al pie de la letra — sobre todo si quieres usar httpResource().

¿Qué significa que Angular 22 sea "signal-first"?

Que ya no es "signals como opción moderna": es el modelo por defecto. Un proyecto nuevo generado con Angular 22 usa OnPush, se apoya en resource()/httpResource() para datos async, y tiene Signal Forms disponible como stack de formularios estable. Sigues pudiendo usar el estilo clásico (Zone.js, RxJS, NgModule), pero ya no es el camino que el framework empuja por defecto.