wandres.dev
RECONCILE Y UNWRAP · diff de datos externos

reconcile + createResource: estado del servidor en el cliente

La culminación del nivel: createResource modela el ciclo asíncrono —cargando, error, último valor—, un store aporta la reactividad de grano fino y reconcile cose cada respuesta nueva en el store preservando la identidad. Juntos forman el patrón canónico del estado del servidor sincronizado en el cliente. Dos formas de implementarlo: un efecto que reconcilia, o la opción storage con un signal profundo donde reaparece unwrap.

⏱ 17 min

Este es el punto donde el nivel se cierra sobre sí mismo. createResource sabe modelar el ciclo de vida de una petición asíncrona —si está cargando, si falló, cuál fue el último valor— pero su valor es un signal plano: cada revalidación lo reemplaza entero, y todo lo que leía data().items se recalcula y remonta. Un store sabe dar reactividad de grano fino, pero no sabe nada de peticiones. reconcile es la costura que une ambos: fusiona cada respuesta del recurso en el store preservando identidad. Juntos —recurso para el ciclo, store para el grano fino, reconcile para la costura, unwrap para cruzar la frontera— forman el patrón industrial del estado del servidor en el cliente.

🎯 Al terminar esta lección sabrás
  • Combinar createResource (ciclo asíncrono) con un store (grano fino) a través de reconcile.
  • Implementar el patrón con un efecto que reconcilia cada nuevo valor del recurso en el store.
  • Usar la opción storage de createResource con un signal profundo para que el recurso sea un store por dentro.
  • Entender por qué unwrap reaparece en el setter de ese signal profundo.

Las dos piezas y su costura

createResource te devuelve un accessor data() más los estados loading y error y las acciones mutate y refetch. Es perfecto para pintar spinners y mensajes de error. Pero data() es un signal atómico: cuando la revalidación trae la colección de nuevo, data() pasa a ser otro objeto entero, y cualquier <For> sobre data().items remonta todas sus filas. El recurso resuelve el cuándo llegan los datos, pero no el cómo se integran sin destruir identidad.

import { createResource } from "solid-js";

const [data] = createResource(cargarTodos);
// data() es atómico: cada refetch reemplaza el valor entero
// <For each={data()?.todos}> remonta TODO en cada revalidación

La pieza que falta es el store, y la costura que las une es reconcile. La idea es sencilla de enunciar: deja que el recurso viva su ciclo de red, pero no leas sus datos directamente; en su lugar, vuelca cada valor que produzca en un store mediante reconcile, y lee siempre del store. El recurso queda como fuente del estado de red; el store, como fuente de los datos.

Patrón A: un efecto que reconcilia

La forma más directa mantiene el recurso y el store como piezas separadas, y las une con un efecto que, cada vez que el recurso entrega un valor nuevo, lo reconcilia dentro del store.

import { createResource, createEffect } from "solid-js";
import { createStore, reconcile } from "solid-js/store";

const [data] = createResource(fuente, cargarTodos);
const [store, setStore] = createStore<{ todos: Todo[] }>({ todos: [] });

createEffect(() => {
  const d = data();
  if (d) setStore("todos", reconcile(d.todos, { key: "id" }));
});

El reparto de responsabilidades queda limpio: tus componentes leen store.todos y disfrutan del grano fino —una fila que no cambió no se remonta en una revalidación—, mientras que data.loading y data.error gobiernan el spinner y el mensaje de fallo. El recurso maneja el ciclo asíncrono; el store, la reactividad; el efecto es la correa que los conecta, y reconcile garantiza que la conexión preserve identidad.

El store parte con un valor inicial coherente —aquí todos vacío—, así que tus componentes nunca leen undefined: mientras la primera carga está en vuelo, store.todos es un array vacío y data.loading vale verdadero. Cuando la respuesta llega, el efecto reconcilia y las filas aparecen sin que hubiera un instante de estado indefinido que manejar. Separar “hay datos” de “están cargando” en dos fuentes distintas —el store y el recurso— es justo lo que evita los ?. defensivos por toda la interfaz.

ℹ️
Lee el estado de red del recurso, los datos del store

La división mental que hace este patrón sólido: pregúntale al recurso por el ciclo de vida —loading, error, cuándo refetch— y pregúntale al store por los datos —store.todos[i].titulo—. Nunca leas data() para pintar la lista, porque eso te reata a su atomicidad; el store existe justamente para desacoplarte de ella.

Patrón B: createDeepSignal como storage

createResource admite una opción storage que te deja sustituir el signal interno donde guarda su valor por uno tuyo. Si le das un signal respaldado por un store que reconcilia en cada escritura, el propio data() se vuelve un store de grano fino por dentro: ya no hace falta el efecto ni el store paralelo.

import { createResource, type Signal } from "solid-js";
import { createStore, reconcile, unwrap } from "solid-js/store";

function createDeepSignal<T>(value: T): Signal<T> {
  const [store, setStore] = createStore({ value });
  return [
    () => store.value,
    (next) => {
      const crudo = unwrap(store.value);
      const v = typeof next === "function" ? next(crudo) : next;
      setStore("value", reconcile(v));
      return store.value;
    },
  ] as Signal<T>;
}

const [data] = createResource(cargarTodos, {
  storage: createDeepSignal,
});
// data() es ahora un store por dentro: data().todos[3].titulo es grano fino

Con este montaje, cada vez que el recurso resuelve y va a guardar su valor, pasa por tu setter, que en lugar de reemplazar reconcilia contra el store interno. El resultado es que data() conserva la ergonomía de un recurso —loading, error, refetch— pero sus lecturas son de grano fino como las de cualquier store. Es, en esencia, lo que hacen por dentro las librerías de datos y el propio SolidStart.

La ventaja del patrón B sobre el A es que la reconciliación deja de ser una responsabilidad que tú orquestas con un efecto y pasa a ser una propiedad intrínseca del recurso: quien lo consume ni siquiera necesita saber que por debajo hay un store. Encapsulas createDeepSignal una vez y lo reutilizas como una primitiva; cada recurso que lo use hereda el grano fino gratis. El coste es una capa más de indirección, y por eso el patrón A sigue siendo preferible cuando quieres que la costura sea visible y explícita en el código.

Ambos patrones se combinan bien con actualizaciones optimistas. Antes de que la red confirme, puedes reflejar el cambio en el store para que la interfaz responda al instante; cuando la respuesta autoritativa llega, reconcile la funde encima y corrige cualquier diferencia respecto a lo que asumiste, sin remontar lo que acertaste. El optimismo pinta rápido, la reconciliación se ajusta a la verdad, y el usuario solo ve una interfaz que reacciona y luego se afina.

Por qué unwrap aquí

El setter de un signal puede recibir un valor directo o una función actualizadora que recibe el valor previo y devuelve el nuevo. Si el previo fuera el proxy del store, esa función lo leería dentro de un contexto reactivo y podría suscribirse sin querer, además de recibir una envoltura en lugar de un objeto llano. Por eso desenvuelves con unwrap antes de llamar al actualizador: le pasas el dato crudo, limpio de proxy y de rastreo. Luego, el resultado se reconcilia de vuelta en el store.

Es el cierre perfecto del nivel, con las cuatro piezas engranando en un solo gesto: unwrap para salir de la reactividad al calcular el próximo valor, reconcile para volver a entrar preservando identidad, key implícita para saber qué filas persisten, y createResource envolviéndolo todo en un ciclo de vida asíncrono. Si omitieras el unwrap, un actualizador funcional recibiría el proxy y correría el riesgo de crear dependencias fantasma o de tratar la envoltura como si fuera el dato; la línea existe precisamente para que el cálculo del siguiente estado ocurra en terreno llano y solo el resultado cruce de vuelta al mundo reactivo.

flowchart TD
S[servidor] --> R[createResource fetcher]
R -->|storage| DS[createDeepSignal]
DS -->|setter recibe valor nuevo| UW[unwrap del previo]
UW --> REC[reconcile en el store interno]
REC --> UI[lecturas de grano fino en la UI]
R -->|loading y error| EST[estados de red para spinner y fallo]
style R fill:#89b4fa,color:#11111b
style REC fill:#a6e3a1,color:#11111b
style UW fill:#f9e2af,color:#11111b
🔁

Patrón A: efecto

Recurso y store separados, unidos por un createEffect que reconcilia. Explícito, fácil de leer y de razonar.

🧬

Patrón B: storage

Un createDeepSignal hace que el propio recurso sea un store por dentro. Compacto y reutilizable como primitiva.

El estado del servidor no es estado del cliente: es una caché de una verdad remota

La confusión que este patrón disuelve de raíz es tratar los datos que vienen del servidor como si fueran estado propio de la aplicación. No lo son. El estado del cliente es tu verdad: nace, vive y muere dentro de la sesión, y tú eres su única autoridad. El estado del servidor es ajeno: es el reflejo local de una verdad que vive en otra parte, que otros pueden cambiar sin avisarte, y que solo conoces a través de instantáneas que pides y que envejecen en cuanto llegan. Modelarlo bien exige separar tres preocupaciones que la ingenuidad mezcla en una sola. La primera es el ciclo de vida asíncrono: una petición que puede estar en vuelo, haber fallado o haber traído su último valor, y que a veces hay que reintentar o revalidar; eso es lo que createResource encapsula, y es la parte que la mayoría reconoce. La segunda, la que casi nadie modela y donde nace el sufrimiento, es la integración con identidad: cada instantánea nueva es un objeto ajeno al que ya tenías, y volcarla sin más arrasa la identidad sobre la que se sostiene toda la reactividad fina, remontando la interfaz en cada revalidación; reconcile sobre un store es lo que convierte esa sucesión de instantáneas inmutables en un grafo vivo que persiste entre refrescos. Y la tercera es el cruce de frontera: para calcular el próximo valor a veces hay que salir del mundo reactivo al mundo llano y volver, y unwrap es el pasaporte de ida —leer el crudo sin suscribir— mientras reconcile es el de vuelta —reentrar preservando identidad—. Cuando las tres piezas encajan, ocurre algo que parece magia y es pura arquitectura: la interfaz refleja la verdad remota, se revalida sola, muestra su carga y sus errores, y jamás parpadea ni pierde el foco de un input al refrescarse, porque por debajo nada se remontó. Eso es lo que las librerías de datos venden empaquetado; entender que por dentro son exactamente recurso más store más reconcile más unwrap es dejar de consumir la abstracción para pasar a dominarla, y poder construirla tú mismo cuando la que existe no encaje.

⚔️ Sincroniza la verdad remota
  1. Monta el patrón A: un createResource que carga una lista y un createEffect que la reconcilia en un store por id.
  2. Revalida con refetch, cambiando un solo campo en la respuesta simulada, y confirma que solo esa fila se actualiza mientras el resto conserva su nodo del DOM.
  3. Pinta un spinner con data.loading y un mensaje con data.error, leyendo siempre los datos desde el store, nunca desde data().
  4. Implementa createDeepSignal y pásalo como storage del recurso; verifica que data().todos ya es de grano fino sin efecto externo.
  5. Explica por qué el setter del signal profundo llama a unwrap antes de aplicar una función actualizadora y qué bug aparecería si no lo hiciera.