Casos de uso de reconcile: red, sockets, revalidación
reconcile brilla exactamente en la costura donde tu aplicación toca el mundo exterior de los datos remotos: cada respuesta de fetch, cada mensaje de WebSocket, cada revalidación en segundo plano trae referencias frescas con contenido casi igual. Tres escenarios canónicos muestran cómo fusionar esas instantáneas en un store vivo sin remontar listas, sin parpadeos y sin perder el estado del DOM.
Todas las fuentes de datos remotos comparten un rasgo que las hace hostiles a la reactividad de grano fino: hablan en instantáneas completas. Un fetch te devuelve el cuerpo entero recién parseado; un mensaje de socket trae un lote nuevo; una revalidación produce un objeto que no comparte una sola referencia con el que ya tenías. Si dejas que cada instantánea reemplace tu store, remontas la interfaz una y otra vez para acabar pintando casi lo mismo. reconcile convierte ese torrente de instantáneas inmutables en actualizaciones quirúrgicas sobre un store vivo. Veamos los tres escenarios donde eso importa más.
- Aplicar
reconcilea respuestas defetchpara revalidar una colección sin remontarla. - Fusionar mensajes de WebSocket en un store vivo conservando la identidad de las filas.
- Revalidar en segundo plano (stale-while-revalidate) sin parpadeos ni pérdida de estado del DOM.
- Elegir entre reemplazo total,
reconcilede rama y escritura por ruta según lo que traiga el mensaje.
Respuestas de fetch: revalidar sin remontar
El caso base es una lista que cargas al montar y vuelves a pedir cada cierto tiempo o al recuperar el foco de la ventana. Sin reconcile, cada revalidación reemplaza el array y remonta todas las filas; con él, solo cambian las que de verdad difieren.
import { createStore, reconcile } from "solid-js/store";
const [state, setState] = createStore({ todos: [] as Todo[] });
async function revalidar() {
const frescos = await fetch("/api/todos").then((r) => r.json());
setState("todos", reconcile(frescos, { key: "id" }));
}
onMount(revalidar);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") revalidar();
});
Lo que ganas es tangible: si el usuario tenía un input a medio escribir en una fila, o la lista scrolleada a cierto punto, o una fila con una animación en curso, todo eso sobrevive a la revalidación porque las filas que no cambiaron conservan su nodo del DOM. La red actualizó los datos sin que el usuario notara ni un parpadeo.
El mismo patrón se aplica a la revalidación disparada por una acción del usuario. Tras un POST que crea o edita un elemento, en vez de insertar a mano en el store puedes volver a pedir la colección y reconciliar: el servidor es la fuente de verdad, y reconcile integra su respuesta tocando solo lo que de verdad cambió respecto a lo que ya mostrabas. Así evitas el clásico bug del estado optimista que diverge de lo que el servidor acabó guardando, porque la instantánea autoritativa se funde con la local sin destruir lo que ambas comparten.
Mensajes de WebSocket: un store vivo
Un socket empuja actualizaciones continuamente, y cada mensaje puede ser una instantánea completa de la colección o un cambio parcial. La decisión clave es qué rama reconciliar según lo que traiga el mensaje.
const [state, setState] = createStore({ mensajes: [] as Msg[] });
const socket = new WebSocket(url);
socket.addEventListener("message", (e) => {
const lote = JSON.parse(e.data) as Msg[];
setState("mensajes", reconcile(lote, { key: "id" })); // instantánea completa
});
onCleanup(() => socket.close());
Cuando el mensaje es la colección entera, reconcilias en la rama de la colección y dejas que el diff descubra qué cambió. Pero cuando el mensaje trae un solo elemento actualizado, reconciliar todo el array es desperdicio: escribes directamente por ruta en ese índice, que ya es una actualización de grano fino sin necesidad de diffear nada.
// El socket manda un único elemento actualizado
socket.addEventListener("message", (e) => {
const uno = JSON.parse(e.data) as Msg;
const i = state.mensajes.findIndex((m) => m.id === uno.id);
if (i >= 0) setState("mensajes", i, reconcile(uno)); // solo esa fila
else setState("mensajes", (arr) => [...arr, uno]); // alta
});
A alta frecuencia de mensajes, esta distinción deja de ser cosmética. Un socket que emite decenas de frames por segundo y reconcilia toda la colección en cada uno malgasta recorridos completos del árbol para tocar una hoja; dirigir cada frame a su rama exacta mantiene el trabajo proporcional al cambio real y no al tamaño de la colección.
Revalidación en segundo plano
El patrón stale-while-revalidate muestra datos cacheados de inmediato y lanza la petición fresca en paralelo, fusionando el resultado cuando llega. reconcile es lo que hace que esa fusión sea invisible: no hay estado de carga que borre la lista ni remonte que la haga parpadear, solo las filas que cambiaron se actualizan cuando la respuesta aterriza.
flowchart LR
C[cache local] --> UI[pinta ya]
R[fetch en segundo plano] --> REC[reconcile en el store]
REC --> D{que difiere}
D -->|filas iguales| K[intactas sin remonte]
D -->|filas nuevas o cambiadas| U[actualiza solo esas]
style C fill:#89b4fa,color:#11111b
style K fill:#a6e3a1,color:#11111b
style U fill:#f9e2af,color:#11111bSin reconcile, este patrón se traiciona a sí mismo: mostrarías la caché al instante para, un segundo después, reemplazarla con la respuesta fresca y provocar justo el parpadeo que querías evitar. La caché y la respuesta casi siempre coinciden en la mayoría de sus filas, así que el diff descubre que hay poquísimo que tocar y la transición de “datos viejos” a “datos frescos” ocurre sin que se note un corte.
Cuando la colección no tiene un identificador estable por elemento —piensa en un array de valores derivados o en filas sin id— reconcile no puede emparejar por clave. En ese caso reconcile(datos, { merge: true }) fuerza el diff hasta las hojas ignorando la identidad de referencia, parcheando por posición en vez de recrear la rama. Es el recurso para conservar algo de granularidad cuando la identidad explícita no existe.
fetch / revalidación
Carga inicial y refresco periódico o al enfocar. Reconcilia la colección entera por su key.
WebSocket
Instantánea completa por mensaje: reconcilia la rama. Elemento suelto: escribe por ruta en su índice.
Qué rama reconciliar
La destreza operativa se reduce a ajustar el ámbito. Reconcilia siempre la rama más estrecha que corresponda al dato que llegó: la colección si vino la colección, el índice si vino un elemento, un subobjeto si vino un fragmento. Cuanto más ajustado el ámbito, menos árbol recorre el diff y menos oportunidades hay de tocar algo que no debía tocarse. Reconciliar la raíz entera por comodidad funciona, pero desperdicia trabajo y, a suficiente frecuencia de mensajes, se nota.
setState(reconcile(instantaneaGlobal)); // raíz: un snapshot completo
setState("todos", reconcile(lista, { key: "id" })); // rama: la colección que llegó
setState("todos", i, reconcile(uno)); // hoja: un elemento por su índice
En la práctica, la mayoría de las integraciones viven en el nivel de rama: el endpoint o el canal corresponde a una colección concreta, y reconcilias esa colección. Reservar la reconciliación de raíz para el arranque o para un snapshot autoritativo completo, y la escritura por índice para los deltas de un elemento, te da un mapa mental claro de a qué altura del store entra cada tipo de mensaje.
Hay un desajuste de impedancias en el corazón de toda aplicación conectada, y casi nadie lo nombra. Los protocolos que traen tus datos —HTTP, WebSocket, cualquier transporte serializado— son incapaces, por su propia naturaleza, de transmitir identidad: mandan bytes que se parsean en objetos nuevos, coherentes consigo mismos pero sin memoria de lo que ya vivía en el cliente. Cada respuesta es una fotografía completa de un instante, y dos fotografías del mismo objeto son, para el runtime, dos objetos distintos que casualmente se parecen. La reactividad de grano fino, en cambio, es pura identidad: su magia entera consiste en que un nodo sigue siendo el mismo mientras el dato conceptualmente persiste, y en propagar cambios solo por las aristas donde algo de verdad cambió. Enchufar directamente un transporte por instantáneas a un grafo por identidad es conectar dos sistemas que no se entienden: el de fuera declara todo nuevo en cada mensaje, y el de dentro, incapaz de reconocer lo que ya tenía, responde remontándolo todo. reconcile es el traductor que reconcilia —literalmente— esas dos gramáticas. Toma la instantánea de fuera y no la deja hablar como reemplazo, sino que la interroga como pregunta: dado lo que ya tengo, ¿qué habría que cambiar para parecerme a esto? Y responde con el conjunto mínimo de mutaciones que lleva el grafo del estado viejo al nuevo, preservando toda la identidad que el contenido permite preservar. Esa operación es lo que hace posible eso que las librerías de datos venden como “server state”: la ilusión de que una verdad remota, que solo sabes consultar por instantáneas, vive sincronizada en el cliente como un grafo reactivo estable. No es magia de la librería; es reconcile haciendo de aduana entre dos maneras irreconciliables de representar el estado en el tiempo, mensaje a mensaje, sin que el usuario vea jamás la costura.
- Carga una lista con
fetch, píntala con<For>y añade un input no controlado por fila; revalida con reemplazo directo y confirma que el input se pierde. - Cambia esa revalidación a
reconcile(..., { key: "id" })y comprueba que el input y el scroll sobreviven. - Simula un WebSocket con un
setIntervalque emite la colección completa con un campo cambiado; reconcilia la rama y verifica que solo esa fila se actualiza. - Añade un segundo tipo de mensaje que trae un único elemento y escríbelo por ruta en su índice en lugar de reconciliar todo el array.
- Monta un stale-while-revalidate: pinta datos cacheados al instante, revalida en segundo plano con
reconciley confirma que no hay parpadeo entre ambos.