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.
Cuando lees state.usuario.nombre de un store no lees un objeto normal: lees a través de un proxy que intercepta el acceso, registra que dependes de esa hoja y te devuelve su valor. Esa mediación es lo que hace posible la reactividad de grano fino, pero también significa que lo que tienes en la mano nunca es el dato desnudo, sino una envoltura reactiva. Hay momentos —pasar los datos a una librería externa, serializarlos sin suscribirte, comparar dos estados— en que esa envoltura estorba y necesitas el objeto crudo que hay debajo. unwrap es la herramienta que lo pela. Y como toda herramienta afilada, corta en las dos direcciones.
- Obtener con
unwrapel objeto plano y no reactivo que envuelve el proxy del store. - Saber cuándo lo necesitas: entregar datos a librerías externas, serializar sin suscribir, comparar instantáneas.
- Entender que
unwrapdevuelve la referencia subyacente real, no una copia: mutarla desincroniza el store. - Distinguir
unwrap—que quita el proxy— de un clon profundo, cuando de verdad quieres aislar el dato.
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.
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 plano
const crudo = unwrap(state); // el objeto subyacente, sin proxy
crudo.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.
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.
Librerías externas
Una librería de gráficos, de validación o de mapas que itera y a veces muta tus datos puede confundirse con un proxy o disparar rastreo sin querer. Le entregas el crudo.
Comparar instantáneas
Para diffear el estado de antes y el de ahora necesitas objetos planos estables, no proxies que se reevalúan al leerse dentro de un ámbito reactivo.
Serializar sin suscribir
Volcar el estado a disco, a un worker o a la red: unwrap te da el objeto para clonar o serializar sin que la lectura te ate a cada hoja.
Depurar
En consola, un proxy se imprime de forma confusa; el crudo se inspecciona como el objeto normal que es.
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.
import { unwrap } from "solid-js/store";
// Pasar a una lib que dibuja a partir de datos planos
grafico.render(unwrap(state.serie));
// Comparar el estado actual con una instantánea guardada
const 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.
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.
Si lo que necesitas es una instantánea independiente que sobreviva a futuras escrituras del store —un “antes” congelado para comparar campo a campo, o datos que otro subsistema pueda mutar sin peligro— unwrap no basta, porque comparte referencias con el store vivo. Ahí clonas: structuredClone(unwrap(state)) te da un objeto plano y aislado. unwrap quita el proxy; el clon corta el cordón umbilical. Son operaciones distintas para intenciones distintas, y confundirlas es la fuente de la mitad de los bugs con stores.
flowchart LR A[objeto crudo] --> B[Proxy del store] B -->|lees state.x| C[suscribe y devuelve valor] B -->|unwrap| A A -->|mutar aqui| D[cambia el dato sin notificar] style B fill:#89b4fa,color:#11111b style A fill:#a6e3a1,color:#11111b style D fill:#f38ba8,color:#11111b
Un store dibuja una frontera invisible dentro de tu programa. A un lado vive el mundo reactivo: objetos que son proxies, lecturas que crean dependencias, escrituras que propagan cambios por un grafo de observadores. Al otro lado vive el mundo llano: el de las librerías que no saben de reactividad, el de la serialización, el de las comparaciones por referencia, el de todo el código que espera un objeto de JavaScript y nada más. unwrap es el paso fronterizo entre ambos, y como toda aduana bien entendida, su valor está tanto en lo que deja pasar como en lo que no. Deja pasar el dato: te entrega el objeto subyacente para que el mundo llano lo lea sin arrastrar consigo la maquinaria reactiva, sin suscribir a nadie, sin proxies que confundan a una librería ajena. Pero no te entrega una copia, y ahí está la disciplina que el nivel exige comprender: lo que cruza la frontera es el original, no un duplicado, y por eso el paso es de un solo sentido. Puedes sacar el dato para leerlo, mostrarlo, medirlo, compararlo; no puedes escribir en él y esperar que el mundo reactivo se entere, porque escribir por fuera del setter es contrabandear un cambio saltándose el único canal que sabe propagarlo. Quien confunde unwrap con un clon acaba con un store cuyo valor y cuya interfaz divergen sin explicación aparente, el peor tipo de bug: silencioso, intermitente, sin excepción que lo delate. Y quien confunde unwrap con una forma de “salir de la reactividad para escribir más cómodo” ha entendido la herramienta al revés: no existe para escribir sin propagar, existe para leer sin suscribir. Interiorizar esa asimetría —lectura libre, escritura prohibida— es interiorizar dónde termina exactamente el territorio que Solid gobierna y dónde empieza el que no.
- Crea un store con un objeto anidado y comprueba en consola que
state.objse imprime como proxy mientrasunwrap(state).objse imprime como objeto llano. - Lee un campo a través de
unwrapdentro de uncreateEffecty verifica que ese efecto no se resuscribe cuando el campo cambia. - Muta a propósito el objeto de
unwrapy observa la desincronización: el valor del store cambia pero el DOM no se actualiza. - Corrige el paso 3 usando el setter del store y confirma que ahora sí se propaga.
- Guarda
structuredClone(unwrap(state))como instantánea, haz varias escrituras y comprueba que el clon no cambió con ellas.