Saltar al contenido

Integrar stores de terceros: Redux, Zustand, RxJS

Todo store externo expone el mismo contrato universal: un getter de instantanea (getState) y un subscribe que avisa de los cambios. El puente con Solid es un signal que el listener escribe tirando de la instantanea, pero un unico signal para todo el estado pierde el grano fino porque cada store devuelve un objeto nuevo entero en cada cambio. La clave es reflejar la instantanea en un createStore de Solid con reconcile, que compara y muta solo las hojas cambiadas, recuperando lecturas granulares sobre un store que sigue siendo la fuente de la verdad.

El contrato universal de un store

Aunque cada librería lo vista distinto, un store externo ofrece siempre dos operaciones: un getter que devuelve la instantánea completa del estado ahora mismo, y un subscribe(listener) que registra una función a la que llamará en cada cambio, devolviéndote una función de baja. El puente con Solid nace de una idea simple: un signal cuya escritura la dispara el listener, tirando de la instantánea fresca en cada aviso.

TSX
import { createSignal, onCleanup, type Accessor } from "solid-js";import type { Store } from "redux";function desdeRedux<S>(store: Store<S>): Accessor<S> {  const [estado, setEstado] = createSignal(store.getState());  const baja = store.subscribe(() => setEstado(() => store.getState()));  onCleanup(baja);  return estado;}

Redux tiene una peculiaridad reveladora: su listener no recibe ningún argumento. Te avisa de que algo cambió y eres tú quien tira de getState() para saber qué. Ese es el patrón pull-tras-push en estado puro —el store empuja una señal de “hay novedad” y tú tiras del valor—, y es exactamente lo que setEstado(() => store.getState()) hace en una línea. Zustand, en cambio, sí pasa el estado nuevo y el anterior a su listener, lo que te deja seleccionar una porción antes de escribir el signal:

TSX
import { createSignal, onCleanup } from "solid-js";function desdeZustand<S, U>(store: StoreApi<S>, selector: (s: S) => U) {  const [porcion, setPorcion] = createSignal(selector(store.getState()));  const baja = store.subscribe((s) => setPorcion(() => selector(s)));  onCleanup(baja);  return porcion; // solo notifica cuando cambia la porcion seleccionada}

El problema del grano fino y la cura con reconcile

El puente básico funciona, pero esconde un coste. Estos stores son inmutables: cada cambio produce un objeto de estado nuevo y completo. Si metes todo ese objeto en un único signal, la igualdad por referencia siempre da distinto, así que cualquier cambio en cualquier rincón del estado notifica a todos los que leen ese signal. Has perdido justo lo que hace especial a Solid: que leer estado.usuario.nombre solo debería reaccionar cuando ese nombre cambie, no cuando cambie un contador en la otra punta del árbol.

La cura es reflejar la instantánea en un createStore de Solid y actualizarlo con reconcile. En lugar de reemplazar el objeto entero, reconcile compara la instantánea nueva contra la actual y muta solo las hojas que de verdad cambiaron. Cada lector se suscribe a su hoja, y solo esa hoja lo despierta.

TSX
import { createStore, reconcile } from "solid-js/store";import { onCleanup } from "solid-js";function espejoDeStore<S extends object>(externo: {  getState: () => S;  subscribe: (fn: () => void) => () => void;}) {  const [estado, setEstado] = createStore<S>(externo.getState());  const baja = externo.subscribe(() =>    setEstado(reconcile(externo.getState())),   // difunde solo lo que cambio  );  onCleanup(baja);  return estado; // un proxy de store con lecturas granulares}

Ahora un componente que lee estado.usuario.nombre se re-ejecuta única y exclusivamente cuando esa hoja cambia, aunque el store externo reconstruya el objeto entero en cada dispatch. Has puesto una capa que traduce “todo es nuevo” en “esto y solo esto cambió”, devolviéndole a Solid su superpoder sobre un estado que gobierna otra librería.

RxJS y la dirección del flujo

Para fuentes de RxJS no hace falta nada nuevo: un BehaviorSubject —que guarda su valor actual— o cualquier observable encajan directamente en el from que ya conoces, porque cumplen el contrato subscribe. La lección de stores es más bien conceptual: un observable de RxJS es un store cuya instantánea viaja con la emisión, mientras que Redux te obliga a tirar de ella aparte.

TSX
import { from } from "solid-js";import { BehaviorSubject } from "rxjs";const sujeto = new BehaviorSubject({ tema: "oscuro" });const ajustes = from(sujeto); // Accessor con el valor actual desde el primer instante

Sea cual sea la librería, respeta una regla de oro: el flujo va en una sola dirección. El store externo sigue siendo la fuente de la verdad y el dueño de las escrituras —sus reducers, su middleware, sus devtools—; Solid es una capa de lectura que refleja. Para cambiar el estado, despachas al store externo por su API de siempre; no escribas el store espejo de Solid a mano, porque el reflejo lo pisaría en el siguiente aviso. Bridge para leer, API original para escribir.

Afinar el reflejo: la opción key de reconcile

reconcile empareja por defecto los objetos por su propiedad id cuando recorre listas, para saber qué elemento de la instantánea nueva corresponde a cuál de la vieja y así mutar en vez de reemplazar. Si tus entidades se identifican por otra clave —un uuid, un sku—, se lo dices con su opción, y si no tienen identidad estable, le pides que compare por posición.

TSX
import { reconcile } from "solid-js/store";// empareja por una clave propia en vez de por idsetEstado(reconcile(externo.getState(), { key: "uuid" }));// para instantaneas sin identidad estable, compara por estructurasetEstado("lista", reconcile(nuevaLista, { key: null }));

Elegir bien la clave es lo que decide si un reordenamiento de la lista externa se traduce en mover nodos —barato, conserva el estado de cada fila— o en recrearlos —caro, pierde foco y scroll—. La misma opción que afina la reconciliación de datos de servidor afina aquí la de un store de terceros: es el mismo motor puesto a un fin distinto, y dominarlo separa un puente que va fino de uno que reconstruye media pantalla en cada dispatch.