Saltar al contenido

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.

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.

TSX
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.

TSX
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.

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.

TSX
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.