Saltar al contenido

unwrap: el objeto crudo detrás del store

Un store es un proxy que rastrea cada lectura. A veces necesitas el objeto plano que ese proxy envuelve: para entregarlo a una librería que no entiende proxies, para serializar sin suscribirte, para comparar una instantánea. unwrap pela el proxy y te da el dato subyacente, pero devuelve la referencia real, no una copia: mutarla desincroniza el store en silencio. Un filo de doble corte que hay que empuñar con criterio.

Qué hay debajo del proxy

createStore envuelve tu objeto en un Proxy que intercepta lecturas y escrituras. Al leer, el proxy suscribe al lector vigente a esa propiedad concreta; al escribir por el setter, notifica a quien la leía. El dato “real” sigue existiendo por debajo, intacto, pero cada vez que lo tocas a través del store pasas por esa capa de mediación.

Esa envoltura es recursiva y perezosa: cada objeto o array anidado se envuelve en su propio proxy la primera vez que alguien lo lee, no de golpe al crear el store. Por eso unwrap tiene trabajo que hacer más allá del nivel superior: para devolverte el dato desnudo debe deshacer la envoltura a lo largo de todo el subárbol que ya estuviera materializado, entregándote objetos y arrays llanos en cada nivel.

TSX
import { createStore, unwrap } from "solid-js/store";const [state] = createStore({ usuario: { nombre: "Ada" }, tags: ["a", "b"] });state.usuario;        // esto es un proxy, no un objeto planoconst crudo = unwrap(state); // el objeto subyacente, sin proxycrudo.usuario.nombre; // lectura normal, SIN crear ninguna suscripción

unwrap recorre el proxy y te devuelve el objeto que envuelve, con sus objetos y arrays anidados también despojados de su envoltura. Leer de ese objeto crudo es leer de memoria normal: no dispara rastreo, no suscribe a nadie, no participa del sistema reactivo. Es, literalmente, tu dato antes de que Solid lo tocara.

Conviene no confundir unwrap con untrack, porque ambos “leen sin suscribir” pero operan en planos distintos. untrack ejecuta una función fuera del rastreo: sigues leyendo a través del proxy, pero esa lectura concreta no crea dependencia. unwrap va un paso más allá y te entrega el objeto sin proxy alguno, de modo que ni esa lectura ni ninguna futura sobre ese objeto podrían suscribir, porque ya no hay proxy que interceptar. Usa untrack cuando quieras una lectura puntual no rastreada dentro del mundo reactivo; usa unwrap cuando quieras sacar el dato entero de ese mundo.

TSX
import { untrack } from "solid-js";import { unwrap } from "solid-js/store";createEffect(() => {  untrack(() => state.usuario.nombre); // lee por el proxy, pero no suscribe  const crudo = unwrap(state);         // saca el objeto entero sin proxy  enviarAServidor(crudo);              // el mundo llano recibe datos normales});

Cuándo necesitas el crudo

Hay varias situaciones canónicas en que el proxy estorba y quieres el objeto desnudo.

El caso de la librería externa es el más frecuente. Muchas librerías reciben un objeto, lo recorren y esperan un objeto llano; si les das un proxy pueden comportarse de forma sutilmente incorrecta, o peor, si lo hacen dentro de un ámbito de rastreo acaban suscribiéndose a medio store sin que nadie lo pidiera. Entregar el crudo corta ese acoplamiento de raíz.

TSX
import { unwrap } from "solid-js/store";// Pasar a una lib que dibuja a partir de datos planosgrafico.render(unwrap(state.serie));// Comparar el estado actual con una instantánea guardadaconst antes = unwrap(state);// ... pasa el tiempo, hay escrituras ...const cambio = unwrap(state) !== antes; // referencia estable para comparar

El caso de la serialización merece una nota fina. JSON.stringify(state) funciona sobre un proxy, pero si lo ejecutas dentro de un efecto o un memo, esa serialización lee todas las hojas y te suscribe a la estructura entera: cualquier cambio en cualquier campo reejecutaría el cómputo. Serializar unwrap(state) en su lugar te da el mismo JSON sin ninguna suscripción, porque estás recorriendo un objeto llano. La diferencia no está en el resultado sino en las dependencias que dejas atrás.

El filo peligroso: no es un clon

Aquí está la trampa que separa a quien entiende unwrap de quien lo usa a ciegas: no devuelve una copia. Devuelve la mismísima referencia que el proxy envuelve. Por eso leer de ella es seguro y barato, pero escribir en ella es escribir en las tripas del store por la puerta de atrás, sin pasar por el setter y, por tanto, sin notificar a nadie.

TSX
const crudo = unwrap(state);crudo.usuario.nombre = "Grace"; // muta el store por debajo, SIN notificar// state.usuario.nombre ya vale "Grace", pero el DOM sigue mostrando "Ada"

El estado y la interfaz quedan desincronizados: el dato cambió pero nadie se enteró. Es uno de los bugs más desconcertantes de depurar precisamente porque todo “parece” correcto —el valor del store es el nuevo, la lógica es la esperada— y sin embargo la pantalla miente. No hay excepción que lo delate, ni advertencia en consola: solo una divergencia silenciosa entre lo que el store cree valer y lo que el DOM muestra. La regla es simple y absoluta: con el objeto de unwrap se lee, nunca se escribe. Para escribir, siempre el setter del store.

El error aparece con más facilidad de lo que parece, porque métodos de array como sort o reverse mutan en el sitio. Llamar unwrap(state.filas).sort(...) reordena el array subyacente del store sin notificar a nadie: la lista queda ordenada en memoria pero la interfaz sigue mostrando el orden viejo hasta que otra escritura la despierte por casualidad. Si quieres ordenar, hazlo sobre una copia y escribe el resultado por el setter, o usa produce para mutar dentro de un borrador rastreado.