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.
Redux, Zustand, XState, un BehaviorSubject de RxJS: por debajo de sus diferencias, todos los stores del ecosistema comparten un mismo contrato mínimo, una instantánea que puedes leer y un subscribe que te avisa cuando esa instantánea cambia. Puentearlos con Solid es, en su forma más simple, un signal que un listener escribe. Pero hay una trampa de rendimiento que separa el puente ingenuo del puente profesional: como estos stores devuelven un objeto nuevo entero en cada cambio, un único signal haría re-ejecutar a todos los lectores por cualquier cambio. La solución elegante es reconcile, que recupera el grano fino de Solid sobre un store que sigue mandando desde fuera.
- Reconocer el contrato universal de un store: instantánea (
getState) mássubscribe. - Construir el puente básico: un signal que el listener escribe tirando de la instantánea.
- Ver por qué un único signal pierde el grano fino y cómo
reconcilelo recupera. - Mantener el store externo como fuente de la verdad y a Solid como capa de lectura reactiva.
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.
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:
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.
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.
flowchart TD E[store externo fuente de la verdad] -->|subscribe avisa| L[listener] L -->|tira de la instantanea| R[setEstado con reconcile] R -->|muta solo hojas cambiadas| S[createStore de solid] S -->|lecturas granulares| C[cada componente lee su hoja] E -->|onCleanup| B[baja de la suscripcion] style R fill:#89b4fa,color:#11111b style S fill:#a6e3a1,color:#11111b style B fill:#f38ba8,color:#11111b
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.
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.
import { reconcile } from "solid-js/store";
// empareja por una clave propia en vez de por id
setEstado(reconcile(externo.getState(), { key: "uuid" }));
// para instantaneas sin identidad estable, compara por estructura
setEstado("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.
Redux
Listener sin argumentos, tiras de getState. Reference nueva en cada dispatch encaja con reconcile.
Zustand
El listener recibe estado y previo, puedes seleccionar una porcion antes de escribir el signal.
RxJS
BehaviorSubject y observables entran por from directamente porque ya cumplen el contrato subscribe.
setEstado(externo.getState()) sin reconcile reemplaza el nodo raíz por el objeto nuevo, y como es otra referencia, invalida a todos los suscriptores del store igual que el signal ingenuo. reconcile es lo que marca la diferencia: recorre en profundidad, empareja por clave —o por la clave que le indiques con su opción key— y aplica solo los deltas. Es el mismo motor que usarías para fusionar datos que llegan de un servidor sin tirar la reactividad; aquí lo pones al servicio de un store de terceros.
Como todo onCleanup, la baja de la suscripción se ata al dueño reactivo que ejecuta el puente. Si construyes el espejo dentro de un componente, se limpia al desmontar. Pero un store global suele quererse una sola vez para toda la app: entonces créalo dentro de un createRoot de larga vida y comparte el accessor o el proxy resultante por contexto, para que el onCleanup tenga un dueño estable y no se dé de baja antes de tiempo ni se filtre.
La tentación al llegar de otro ecosistema es reescribir el estado global en signals y jubilar el store externo. Casi siempre es un error. Ese store no es solo un contenedor de datos: es un contrato que arrastra middleware, devtools con viaje en el tiempo, lógica de reducers probada, integraciones que tu equipo ya entiende. Tirarlo para ganar reactividad fina es pagar un precio altísimo por algo que puedes obtener puenteando. Y el puente correcto descansa sobre dos verdades. La primera es que todo store, por debajo de su fachada, expone el mismo contrato mínimo —una instantánea legible y un subscribe que avisa—, de modo que el puente que aprendes para Redux sirve, con retoques cosméticos, para Zustand, para XState o para el store propietario que tu empresa escribió hace años. La segunda es que el enemigo del rendimiento no es el puente sino el grano grueso: como estos stores producen un objeto nuevo entero en cada cambio, un único signal convierte cada modificación en una invalidación global, y ahí se evapora la ventaja de Solid. reconcile es la pieza que restaura el orden, porque traduce el “todo es nuevo” de la inmutabilidad estructural en el “solo esto cambió” que Solid necesita para despertar únicamente a quien de verdad depende del dato. El resultado es una arquitectura honesta y de flujo único: el store externo manda y escribe, Solid escucha y refleja con precisión quirúrgica, y las escrituras siguen yendo por la puerta de siempre. Cuando interiorizas este patrón dejas de ver un dilema entre “el estado de Solid” y “el estado de la otra librería” y empiezas a ver capas: una fuente de la verdad abajo, una proyección reactiva y granular encima, y un reconcile de por medio que hace que ambas convivan sin que ninguna renuncie a lo que hace bien.
- Escribe
desdeReduxy úsalo con un store real; confirma que el accessor se actualiza en cadadispatch. - Adáptalo a Zustand pasando un
selectory demuestra que el signal solo notifica cuando cambia la porción seleccionada. - Mete todo el estado en un único signal y mide: cambia una rama y observa cómo se re-ejecutan lectores de ramas ajenas.
- Sustituye ese signal por un
createStoreactualizado conreconcile; repite la prueba y verifica que solo reacciona el lector de la hoja cambiada. - Intenta escribir el store espejo de Solid a mano, provoca el siguiente aviso del store externo y observa cómo el reflejo lo pisa; concluye por qué las escrituras deben ir por la API original.