wandres.dev
INTEGRAR LIBRERÍAS · interoperar con JS

observable(): exponer un signal como observable

observable toma un accessor de Solid y devuelve un objeto que implementa el protocolo de observables de TC39, con un metodo subscribe y la clave Symbol.observable, de modo que RxJS y cualquier libreria compatible pueden consumir tu signal como si fuera una fuente nativa. Es el dual exacto de from: donde from trae el mundo push hacia dentro, observable expone el mundo pull hacia fuera, y juntos permiten el viaje de ida y vuelta signal, operadores de RxJS, signal otra vez.

⏱ 15 min

En la lección anterior trajiste fuentes externas hacia dentro de Solid con from. observable hace el viaje contrario: toma un Accessor de Solid y lo envuelve en un objeto que cualquier consumidor de observables —RxJS a la cabeza— reconoce como una fuente nativa. Bajo el capó implementa el protocolo de observables de TC39: un método subscribe que arranca un efecto por cada suscriptor y la clave especial Symbol.observable que anuncia “soy interoperable”. Con from y observable en las manos, la frontera entre el grafo de Solid y el universo de RxJS deja de ser un muro y pasa a ser una puerta giratoria.

🎯 Al terminar esta lección sabrás
  • Entender que observable convierte un Accessor de Solid en un observable push para el exterior.
  • Reconocer que implementa el protocolo TC39: subscribe(observer) y la clave Symbol.observable.
  • Consumir un signal desde RxJS con su from para aplicar operadores como debounceTime o map.
  • Ver from y observable como duales y encadenar el viaje de ida y vuelta entre ambos mundos.

Un signal visto desde fuera

observable recibe un accessor y devuelve un objeto con dos capacidades. La primera es un método subscribe(observer), donde observer sigue la forma de TC39 —un objeto con next, error y complete, o directamente una función que hace de next—. Al suscribirte, observable monta por dentro un efecto reactivo aislado en su propio createRoot: lee el accessor, llama a observer.next con el valor actual de inmediato, y vuelve a llamarlo cada vez que el signal cambia. Te devuelve un objeto con unsubscribe que dispone ese efecto.

import { createSignal, observable } from "solid-js";

const [cuenta, setCuenta] = createSignal(0);
const cuenta$ = observable(cuenta);

const sub = cuenta$.subscribe((v) => console.log("emite:", v)); // emite: 0 ya
setCuenta(1); // emite: 1
setCuenta(2); // emite: 2
sub.unsubscribe();

La segunda capacidad es la clave Symbol.observable: el objeto devuelto se implementa a sí mismo bajo esa clave. Ese es el apretón de manos del protocolo TC39, el que permite que una librería ajena diga “dame tu observable” sin saber nada de Solid.

El protocolo TC39 y la interoperabilidad con RxJS

Symbol.observable es una propuesta de estándar que fija un contrato mínimo para que observables de distintas librerías se entiendan. RxJS lo respeta: su propia función from sabe reconocer cualquier objeto que exponga Symbol.observable y adoptarlo como una fuente nativa. Ahí está la pieza que abre todo el catálogo de operadores de RxJS a un signal de Solid.

import { createSignal, observable } from "solid-js";
import { from as rxFrom } from "rxjs";
import { debounceTime, map, distinctUntilChanged } from "rxjs/operators";

const [texto, setTexto] = createSignal("");

const busqueda$ = rxFrom(observable(texto)).pipe(
  map((t) => t.trim()),
  distinctUntilChanged(),
  debounceTime(300),
);

busqueda$.subscribe((consulta) => lanzarBusqueda(consulta));

Fíjate en lo que ocurre: texto es un signal de Solid corriente, editado por un input; observable(texto) lo expone; el from de RxJS lo adopta; y a partir de ahí operan map, distinctUntilChanged y debounceTime, que son territorio donde RxJS brilla. Has delegado la coreografía temporal —recortar, deduplicar, antirebotar— a la herramienta que mejor la expresa, sin sacar el estado del grafo de Solid.

flowchart LR
S[accessor de solid] --> O[observable lo expone push]
O --> X[rxjs from lo adopta]
X --> P[pipe con map debounce filter]
P --> B[de vuelta a solid con from]
style O fill:#89b4fa,color:#11111b
style P fill:#f9e2af,color:#11111b
style B fill:#a6e3a1,color:#11111b

Duales: el viaje de ida y vuelta

from y observable son operaciones inversas, y verlas como par es lo que desbloquea el patrón más potente: el viaje redondo. Sacas un signal a RxJS con observable, aplicas los operadores que quieras, y traes el resultado de vuelta a Solid con from. El estado nunca deja de ser reactivo dentro de Solid; solo hace una excursión por RxJS para aprovechar sus operadores temporales.

import { createSignal, from, observable, createEffect } from "solid-js";
import { from as rxFrom } from "rxjs";
import { debounceTime } from "rxjs/operators";

const [texto, setTexto] = createSignal("");

// solid -> rxjs -> operadores -> solid
const textoDebounced = from(
  rxFrom(observable(texto)).pipe(debounceTime(300)),
);

createEffect(() => console.log("estable:", textoDebounced())); // Accessor de nuevo

El resultado, textoDebounced, vuelve a ser un Accessor<string | undefined> que lees en el grafo como cualquier signal. Las dos fronteras —salida con observable, entrada con from— son simétricas, y esa simetría es la que hace que integrar RxJS en Solid no sea un parche sino una composición limpia.

La forma del observer y sus límites

El observer que pasas a subscribe sigue la forma de TC39: un objeto con next, error y complete, o una función suelta que hace de next. observable invoca next en cada cambio del signal, pero conviene conocer sus límites para no llevarse sorpresas al componer con operadores ajenos.

const sub = observable(cuenta).subscribe({
  next: (v) => pintar(v),
  error: (e) => console.error(e),     // rara vez se dispara solo
  complete: () => console.log("fin"), // nunca por su cuenta
});

Dos límites importan. El primero: observable no emite complete por iniciativa propia, porque un signal no “termina” —vive mientras viva su dueño—; la secuencia se corta cuando llamas a unsubscribe o cuando el dueño se dispone. El segundo: si leer el accessor lanza una excepción, esta se propaga como un error normal de JavaScript en vez de enrutarse automáticamente al canal error del observer, así que un catchError de RxJS aguas abajo no lo verá salvo que lo captures tú. Son las costuras esperables de un puente entre dos modelos de error distintos, y saberlas evita depurar a ciegas cuando el flujo cruzado no se comporta como un observable puro.

🔌

subscribe TC39

El objeto expone subscribe con observer next error complete, la forma que espera cualquier consumidor estandar.

🤝

Symbol.observable

La clave de interoperabilidad que hace que el from de RxJS reconozca tu signal como fuente nativa.

🔁

Dual de from

observable saca push hacia fuera, from trae push hacia dentro, y el viaje redondo los encadena.

ℹ️
Cada suscripción es caliente y emite el valor actual al instante

El observable de observable no es perezoso a la manera clásica: en cuanto te suscribes, arranca un efecto que lee el accessor y te entrega el valor actual de inmediato, antes de cualquier cambio. Cada suscripción monta su propio createRoot con su efecto, así que dos suscriptores son dos efectos independientes. Y no completa por sí solo —no hay un complete natural en un signal, que vive mientras viva su dueño—; te desenganchas con unsubscribe o dejando que el dueño se disponga.

📝
Symbol.observable es un protocolo compartido, no un invento de Solid

La clave Symbol.observable viene de una propuesta de estándar de TC39 pensada para que observables de librerías distintas se reconozcan sin acoplarse. Como aún no es un símbolo nativo en todos los entornos, el ecosistema usa un ponyfill —un Symbol.observable real si existe, o la cadena @@observable si no—, y tanto Solid como RxJS respetan esa convención. Por eso el apretón de manos funciona: no es que Solid hable el idioma de RxJS ni al revés, es que ambos hablan un tercer idioma común. Cuando integres una librería nueva, busca si expone Symbol.observable: si lo hace, from y observable la entienden sin adaptador.

⚠️
No es un reemplazo de los memos ni de los efectos

observable existe para interoperar, no para hacer reactividad dentro de Solid. Si tu problema se resuelve con un createMemo o un createEffect, úsalos: son más directos, más baratos y no arrastran una dependencia externa. Saca un signal a observable solo cuando de verdad necesitas la maquinaria de otra librería —los operadores de RxJS, un consumidor que ya habla el protocolo TC39— al otro lado de la frontera.

Signal y observable son dos vistas del mismo valor que cambia en el tiempo

Un signal y un observable no son cosas distintas: son dos representaciones del mismo concepto, un valor que varía en el tiempo, contadas desde los dos lados de la dualidad push–pull. Un observable lo cuenta empujando: define una secuencia de emisiones y te avisa en cada una. Un signal lo cuenta tirando: mantiene el valor presente y registra quién lo lee para invalidarlo cuando cambie. Son duales en el sentido matemático fuerte —lo que en uno es una emisión, en el otro es una notificación de invalidación; lo que en uno es suscribirse, en el otro es leer dentro de un scope de rastreo—, y por eso existe una traducción exacta en ambos sentidos. from implementa la traducción push a pull colocando un signal como buffer; observable implementa la traducción pull a push colocando un efecto como emisor. Que el objeto devuelto lleve Symbol.observable es la parte más profunda: significa que Solid no inventó su propia frontera, sino que se enchufó a un protocolo compartido por todo el ecosistema, de modo que la interoperabilidad no es un favor que Solid le hace a RxJS sino un estándar que ambos honran. Cuando ves las dos primitivas como el par que son, dejas de preguntarte “cómo meto RxJS en Solid” y entiendes que no hay un dentro y un fuera con un muro entre medias, sino un único valor que cambia en el tiempo y dos idiomas para hablar de él, con un diccionario de ida y vuelta que cabe en dos funciones. Esa es la marca de un diseño maduro: no encerrarse, sino exponer la dualidad y dejar que el valor cruce la frontera tantas veces como el problema lo pida.

⚔️ Cruza la frontera en los dos sentidos
  1. Crea un signal, envuélvelo con observable y suscríbete con una función; confirma que recibes el valor actual de inmediato y luego cada cambio.
  2. Suscríbete dos veces al mismo observable y comprueba que ambas suscripciones reciben las emisiones de forma independiente.
  3. Usa el from de RxJS sobre observable(signal) y aplica un pipe con debounceTime(300) para antirebotar las emisiones de un input.
  4. Cierra el círculo: envuelve ese pipe con el from de Solid y lee el resultado como un Accessor dentro de un createEffect.
  5. Llama a unsubscribe y demuestra que las emisiones se detienen; razona por qué el observable nunca emite complete por su cuenta.