wandres.dev
OBSERVABLES · RxJS y streams

Signal contra observable

Dos primitivos reactivos que se confunden y no deberían: el signal es un valor que siempre tiene un estado actual, el observable es un stream de eventos en el tiempo. Cuándo usar cada uno, cómo interoperan y por qué 2026 los quiere a los dos.

⏱ 18 min

La pregunta más malentendida de la reactividad moderna es “signal u observable, cuál es mejor”. Es una falsa dicotomía: modelan cosas distintas. Un signal responde a “cuál es el valor ahora”; siempre tiene un estado actual, legible de forma síncrona. Un observable responde a “qué valores han ido ocurriendo”; es un flujo de eventos en el tiempo que puede no tener valor presente. Confundirlos lleva a usar un mazo para una foto o una foto para un río. Esta lección, cierre del nivel, te da el criterio para elegir y para hacerlos convivir.

🎯 Al terminar esta lección sabrás
  • Distinguir el signal (estado actual, pull) del observable (stream, push).
  • Reconocer qué problemas pide cada primitivo.
  • Interoperar entre ambos con toSignal y toObservable.
  • Elegir con criterio en el ecosistema de 2026.

Estado que es contra eventos que ocurren

La diferencia se capta en una frase: un signal es un valor, un observable describe una secuencia de valores. El signal es un contenedor con un dato siempre presente; lo lees y obtienes algo ya. El observable es una receta perezosa de emisiones; hasta que no te suscribes no hay nada, y aun suscrito puede que no haya emitido todavía.

// Signal (Angular / propuesta TC39): siempre hay un valor AHORA
const contador = signal(0);
contador();            // 0  -> lectura sincrona, siempre da algo
contador.set(1);       // escritura directa
const doble = computed(() => contador() * 2);  // derivacion memoizada

// Observable: una secuencia en el TIEMPO, sin valor presente garantizado
const clics$ = fromEvent(boton, 'click');   // aun no hay ningun valor
clics$.subscribe((e) => console.log(e));    // los valores llegaran... o no

El signal se lee llamándolo; no hay suscripción, no hay next, no hay ciclo de vida que limpiar. Su reactividad es pull con seguimiento automático de dependencias: computed sabe qué signals leyó y se recalcula solo cuando esos cambian. El observable es push: la fuente empuja emisiones y tú reaccionas, con toda la maquinaria de suscripción y cancelación que ya conoces.

Hay un matiz técnico que da al signal una virtud difícil de exagerar: es glitch-free. En un grafo en diamante —dos derivadas que dependen de una misma fuente, y una tercera que depende de ambas— un sistema push ingenuo recalcula la tercera dos veces y puede exhibir un valor intermedio incoherente. El modelo pull del signal evalúa bajo demanda y garantiza que nunca observarás un estado a medio actualizar. Para lo que se pinta en pantalla, esa coherencia instantánea es exactamente lo que quieres.

El signal también tiene su forma de efectos: effect, una función que se reejecuta cuando cambia cualquier signal que lee en su interior. Es el análogo del subscribe de un observable, pero sin gestión manual de cancelación: el framework lo desmonta junto con el componente que lo creó.

const tema = signal<'claro' | 'oscuro'>('claro');
effect(() => { document.body.dataset.tema = tema(); }); // reacciona a cada cambio

El cuadro de las cuatro diferencias

flowchart TB
subgraph Signal
  S1[valor actual siempre presente]
  S2[pull con tracking automatico]
  S3[sincrono y sin glitches]
  S4[sin suscripcion ni teardown]
end
subgraph Observable
  O1[secuencia en el tiempo]
  O2[push desde la fuente]
  O3[async y multivalor]
  O4[suscripcion cancelable]
end

Cada eje decide un tipo de problema. “Valor actual siempre presente” hace del signal el primitivo natural del estado de UI: lo que se pinta en pantalla siempre tiene un valor concreto. “Secuencia en el tiempo” hace del observable el primitivo natural de los eventos: un clic no tiene “valor actual”, ocurre o no ocurre. La ausencia de suscripción en el signal elimina de un plumazo la clase entera de bugs de fugas por no desuscribir; a cambio, el signal no sabe expresar “el tercer clic tras 300 ms sin teclear”, que es pan comido para un observable.

💡
La heurística de una línea

Si puedes preguntar “cuál es su valor ahora mismo” y la pregunta tiene sentido, es un signal: el nombre del usuario, si el panel está abierto, el total del carrito. Si la pregunta natural es “qué ha pasado y cuándo”, es un observable: pulsaciones de tecla, mensajes de un websocket, respuestas de red en curso. Estado se modela con signals; eventos, con observables.

Cuándo cada uno

En la práctica de 2026, el reparto es bastante nítido. Los signals han ganado el estado de interfaz porque su modelo síncrono encaja con el renderizado: SolidJS los tiene como núcleo, Angular los adoptó desde la v16, Vue los llama refs, Svelte 5 los expone como runes y la propuesta TC39 los encamina hacia el estándar del lenguaje. Los observables retienen el dominio de lo que tiene forma de flujo temporal:

🎯

Signal: estado y derivación

Estado local y compartido de UI, valores derivados con computed, lo que se lee de forma síncrona en el renderizado. Sencillo, sin ceremonia.

🌊

Observable: streams y tiempo

Eventos, websockets, coordinación asíncrona, operadores temporales como debounceTime, cancelación en cascada con switchMap. Potencia sobre el tiempo.

El contraste se ve en código. El mismo contador que como signal es una línea, como observable exige un Subject, una suscripción y su limpieza:

// Estado con signal: leer es sincrono, derivar es trivial
const items = signal<Item[]>([]);
const total = computed(() => items().reduce((s, i) => s + i.precio, 0));

// El mismo estado con observable: mas ceremonia para algo que "solo es un valor"
const items$ = new BehaviorSubject<Item[]>([]);
const total$ = items$.pipe(map((xs) => xs.reduce((s, i) => s + i.precio, 0)));

La regla degenerada ayuda a decidir en los bordes: un observable que emite un solo valor y guarda el último es, en esencia, lo que un signal hace mejor y con menos peso. Un signal al que quieres aplicarle debounceTime o cancelación temporal está pidiendo a gritos ser un observable durante ese tramo. No hay ganador universal; hay adecuación al problema.

📝
El signal solo propaga cuando el valor cambia de verdad

Un detalle que sorprende a quien viene de observables: el signal compara por identidad y no notifica si el nuevo valor es igual al anterior. Escribir set con el mismo valor no dispara nada, y por eso las derivaciones no se recalculan de más. Para objetos puedes proveer una función de igualdad propia. Un observable, en cambio, emite lo que le mandes emitir; si quieres el mismo comportamiento necesitas distinctUntilChanged de forma explícita.

Interoperar, no elegir bando

El error de novato es tribalismo: “somos casa de signals” o “somos casa de RxJS”. El ecosistema maduro los teje. Angular ofrece el puente canónico en ambos sentidos, y ese puente es la respuesta práctica a casi todo:

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

// Observable -> signal: consumir un stream como estado sincrono en la vista
const usuario = toSignal(usuario$, { initialValue: null });

// Signal -> observable: llevar estado al mundo de los operadores temporales
const busqueda = signal('');
const resultados = toSignal(
  toObservable(busqueda).pipe(debounceTime(300), switchMap(buscar)),
  { initialValue: [] },
);

Fíjate en el patrón: el estado de entrada vive como signal porque es “el texto actual”; se convierte en observable justo para el tramo que necesita tiempo y cancelación (debounceTime, switchMap); y el resultado vuelve a signal para pintarse. Cada primitivo hace lo que sabe hacer, y el puente los cose. Ese es el diseño reactivo de 2026: signals para el estado, observables para el flujo, interoperación en las costuras. El único abuso a evitar es convertir de más: envolver todo en conversiones constantes esconde qué es estado y qué es evento, justo la distinción que da claridad.

Son duales, no rivales: pull contra push sobre el mismo grafo

Signal y observable son las dos caras de la misma moneda de la reactividad, y verlas como dual en lugar de como enemigas es el salto conceptual de esta lección. Ambos construyen un grafo de dependencias donde un cambio se propaga a lo que depende de él; difieren solo en la dirección de la propagación y en la relación con el tiempo. El signal es pull: no calcula nada hasta que lo lees, memoiza el resultado, garantiza un valor presente y coherente en todo instante, y por eso es glitch-free y perfecto para lo que se pinta. El observable es push: la fuente dicta el ritmo, modela la llegada de valores como eventos discretos en el tiempo y por eso captura naturalmente lo que un signal no puede: la dimensión temporal, la multiplicidad, la cancelación de trabajo en vuelo. Un signal es un observable colapsado a su valor actual; un observable es un signal desplegado en su historia temporal. La madurez profesional no consiste en elegir un primitivo y despreciar el otro, sino en leer cada pieza de tu sistema y preguntarte si es estado —algo que siempre tiene un ahora— o evento —algo que ocurre en el tiempo—, y usar el primitivo que hace esa naturaleza explícita. Quien piensa así deja de discutir modas y empieza a modelar la realidad de su dominio con el filo adecuado. Ahí termina este nivel y empieza el diseño reactivo de verdad.

⚔️ Modela con el primitivo correcto
  1. Toma una pantalla real y clasifica cada dato: cuáles son estado (signal) y cuáles son eventos (observable). Justifica cada elección.
  2. Implementa un buscador donde el texto sea signal, el tramo temporal sea observable y el resultado vuelva a signal.
  3. Coge un BehaviorSubject usado como estado y reescríbelo como signal; mide cuánta ceremonia de suscripción desaparece.
  4. Argumenta en tres frases por qué “signal contra observable” es una falsa dicotomía y da un ejemplo donde forzar el primitivo equivocado te complicaría la vida.