La opción key de reconcile: identidad para diffear
Al diffear dos arrays, reconcile tiene que responder una pregunta que el contenido por sí solo no contesta: este objeto nuevo, ¿es el mismo elemento de antes con cambios, o uno genuinamente distinto? La opción key le dice qué propiedad lleva la identidad estable. Acertar con key hace que las listas se diffeen a la perfección; equivocarse fuerza a recrear todo o, peor, corrompe la identidad emparejando lo que no debía.
Cuando reconcile compara el array viejo con el nuevo, se enfrenta a un problema que no tiene solución a partir del contenido: dado un objeto en la lista nueva, ¿con cuál de la lista vieja se corresponde? No puede emparejarlos por contenido, porque justamente el contenido es lo que quizá cambió. No puede fiarse de la posición, porque los elementos se reordenan. Necesita que tú le señales qué propiedad de cada objeto es su identidad estable a través del tiempo. Esa es la misión de key: nombrar el campo que dice este es el mismo elemento, aunque todo lo demás en él haya cambiado.
- Entender el problema que
keyresuelve: emparejar los elementos de la lista vieja con los de la nueva. - Configurar
key—por defecto"id"— para quereconcilediffee arrays de objetos correctamente. - Ver qué ocurre con
key: nully cuándo el emparejado cae en posición o referencia. - Conocer la opción
mergey su relación conkeycuando no existe una identidad estable.
El problema del emparejado
Diffear dos listas es, en el fondo, resolver un emparejamiento: decidir qué elemento nuevo es la continuación de qué elemento viejo. Cada emparejamiento posible lleva a un resultado distinto. Si reconcile empareja bien, parchea en su sitio los que cambiaron, mueve los que se reordenaron conservando su identidad, crea los nuevos y elimina los ausentes. Si empareja mal, puede acabar recreando filas que solo se movieron, o volcando los datos de un elemento sobre el nodo de otro.
Dos estrategias ingenuas fracasan aquí. Emparejar por contenido es circular: usarías como ancla justo lo que pudo cambiar, así que una fila editada parecería nueva y su original, una baja. Emparejar por posición se rompe en cuanto la lista se reordena o se inserta algo al principio: todo lo de debajo se desplaza un puesto y se leería como si cada fila hubiera cambiado a la vez. La única ancla fiable es una propiedad que viaja con el elemento y no depende ni de su contenido mutable ni de su lugar. Esa propiedad es la key.
flowchart TD
V[lista vieja: a1 b2 c3] --> K{empareja por key}
N[lista nueva: b2 a1 d4] --> K
K -->|a1 con a1| M1[misma identidad se mueve]
K -->|b2 con b2| M2[misma identidad se mueve]
K -->|c3 sin pareja| R[elimina c3]
K -->|d4 sin pareja| C[crea d4]
style M1 fill:#a6e3a1,color:#11111b
style M2 fill:#a6e3a1,color:#11111b
style R fill:#f38ba8,color:#11111b
style C fill:#89b4fa,color:#11111bSobre datos concretos se ve el mecanismo entero. Estas dos listas difieren en orden, en contenido y en altas y bajas a la vez, y aun así reconcile sabe exactamente qué hacer con cada elemento en cuanto le das la clave:
const vieja = [{ id: "a", n: 1 }, { id: "b", n: 2 }, { id: "c", n: 3 }];
const nueva = [{ id: "b", n: 9 }, { id: "a", n: 1 }, { id: "d", n: 4 }];
// Con key "id": "a" se mueve intacto, "b" se mueve y parchea n, "c" se elimina, "d" se crea
key: la propiedad de identidad
key es el nombre de la propiedad que reconcile lee de cada objeto para emparejarlo. Por defecto vale "id", así que si tus datos ya usan ese campo no necesitas configurar nada. Si tu identidad vive en otro campo —uuid, _id, sku— se lo indicas explícitamente.
import { reconcile } from "solid-js/store";
setState("filas", reconcile(frescas)); // empareja por la propiedad id
setState("filas", reconcile(frescas, { key: "uuid" })); // empareja por uuid
setState("filas", reconcile(frescas, { key: "sku" })); // empareja por sku
Con la key correcta, dos filas con el mismo valor de clave se consideran el mismo elemento: reconcile parchea en el nodo existente solo los campos que difieren y conserva su referencia, su nodo del DOM y su lugar en el <For>. Filas con claves que ya no aparecen se eliminan; claves nuevas se crean. Un reordenamiento puro no recrea nada: mueve.
reconcile aplica la misma key en todos los niveles del árbol donde encuentre arrays de objetos. Si tu estructura anida colecciones —una lista de grupos, cada uno con su lista de miembros— y todas usan id como identidad, una sola key las cubre a todas. Es una convención implícita que conviene mantener: un mismo nombre de campo para la identidad en todo el dominio simplifica cada reconciliación y evita tener que pensar por rama.
La primera reconciliación sobre un store vacío es un caso degenerado sano: como no hay nada viejo con que emparejar, cada elemento entrante simplemente se crea. A partir de ahí, cada reconciliación posterior ya tiene contra qué diffear, y es cuando la key empieza a ganar su sueldo preservando lo que persiste entre una respuesta y la siguiente.
// key equivocada: apunta a un campo que cambia -> todo parece nuevo y remonta
setState("filas", reconcile(frescas, { key: "actualizado" }));
// key correcta: apunta al identificador inmutable -> preserva lo que persiste
setState("filas", reconcile(frescas, { key: "id" }));
La key debe ser única y estable. Si dos elementos distintos comparten el mismo valor de clave, reconcile los tratará como el mismo y volcará los datos de uno sobre el nodo del otro: identidad corrompida, interfaz inconsistente. Y si la clave cambia entre revalidaciones para el “mismo” elemento conceptual, se verá como una baja y un alta, remontando la fila. Elige como clave algo que nazca con el elemento y no cambie nunca: el identificador del servidor, no un índice ni un campo editable.
key null y el emparejado posicional
A veces no hay ninguna propiedad de identidad porque los elementos no son objetos con id, sino primitivos —números, cadenas— o tuplas de forma fija donde la posición es la identidad. Para esos casos, key: null desactiva el emparejado por clave y reconcile compara por posición.
// Array de primitivos: la posición es la identidad
setState("puntuaciones", reconcile(nuevas, { key: null }));
// Tupla de forma fija: cada ranura tiene un significado por su lugar
setState("rango", reconcile([min, max], { key: null }));
Con key: null no hay noción de “esta fila se movió allá”: la ranura 0 se compara con la ranura 0. Es lo correcto cuando el orden porta significado y no esperas reordenamientos, pero sería un error sobre una lista de objetos reordenables, porque cualquier cambio de orden se leería como que todas las posiciones cambiaron a la vez, remontando de facto toda la lista.
merge: cuando no hay clave pero quieres granularidad
merge gobierna cuán profundo baja el diff. Con merge: false —el valor por defecto— reconcile hace comprobaciones por referencia: si una rama del árbol nuevo es referencialmente igual a la vieja la conserva entera, y donde difieren se apoya en key para preservar o reemplazar subárboles. Con merge: true ignora la identidad de referencia y fuerza el diff hasta las hojas, parcheando propiedad a propiedad por posición aunque no haya clave que emparejar.
setState("filas", reconcile(frescas, { key: "id" })); // identidad explícita
setState("valores", reconcile(nums, { key: null })); // posicional, primitivos
setState("datos", reconcile(frescos, { merge: true })); // sin key estable, diff a la hoja
La regla práctica: si hay un identificador estable, dáselo por key y deja merge en falso —es lo más granular y correcto—. Si no hay identidad posible pero aun así quieres conservar nodos y parchear hojas en lugar de recrear ramas enteras, merge: true es el recurso. Y si el orden es la identidad, key: null.
Ten presente que merge: true es más caro y menos preciso que un buen key: al renunciar a la identidad, no puede reconocer que una fila simplemente se movió, así que un reordenamiento se convierte en un parcheo posicional que toca muchas más hojas de las necesarias. Es una red de seguridad para datos sin identidad natural, no un sustituto de modelar bien la clave.
key: campo
Hay identidad estable por elemento. Empareja por esa propiedad y preserva filas entre revalidaciones. El caso común.
key: null
No hay identidad y la posición es el significado. Compara ranura con ranura. Para primitivos y tuplas fijas.
merge: true
Ni clave ni posición como identidad, pero quieres parchear hojas en vez de recrear ramas. La red de seguridad.
Tres preguntas encadenadas resuelven cualquier caso. ¿Hay una propiedad de identidad estable por elemento? Si sí, pásala como key y termina. Si no, ¿la posición porta la identidad, sin reordenamientos esperados? Entonces key: null y diff posicional. Y si ni hay clave ni la posición es identidad, pero prefieres parchear hojas a recrear ramas, merge: true. La inmensa mayoría de las colecciones del mundo real caen en el primer caso; los otros dos son la salida para datos sin identidad natural.
Es tentador creer que un elemento “tiene” una identidad, como tiene un color o un precio, y que reconcile simplemente la lee. Pero la identidad no vive en el dato: vive en la pregunta que tú decides hacerle al dato. Dos filas de una tabla que comparten todos sus campos, ¿son el mismo elemento visto dos veces o dos elementos idénticos? Un objeto cuyo id cambió pero cuyo contenido es el mismo, ¿persistió o es otro? El dato no contesta; tú contestas, al elegir qué propiedad nombra la identidad. Por eso key no es un detalle de configuración sino una declaración semántica: estás diciendo qué significa que dos cosas sean la misma a lo largo del tiempo en tu dominio. <For> responde esa pregunta implícitamente, con la identidad de referencia de JavaScript: dos filas son la misma si son el mismo objeto en memoria. reconcile te obliga a hacerla explícita y basada en contenido, porque los datos que llegan de la red nunca comparten referencia con los que tenías, y sin una clave de contenido no habría forma de reconocer que la fila del servidor de hace un segundo y la de ahora son la misma fila que cambió. Elegir bien la clave es, entonces, un acto de modelado, no de sintaxis: es decidir cuál es el eje de continuidad de cada entidad de tu dominio, ese campo que nace con ella, no cambia jamás y sobrevive a cualquier edición de lo demás. Aciértalo y tus listas se moverán, crecerán y se editarán conservando identidad como seres vivos; fállalo y verás filas remontarse sin motivo o, en el peor caso, la identidad de un elemento derramarse sobre otro. La clave no describe tus datos: decide qué son.
- Reconcilia una lista de objetos con
idsin pasar opciones y confirma que empareja poridpor defecto. - Renombra el campo a
uuid, comprueba que el emparejado se rompe y arréglalo con{ key: "uuid" }. - Reordena la lista nueva y verifica que las filas se mueven conservando su nodo del DOM en lugar de remontarse.
- Provoca a propósito una
keyduplicada entre dos elementos distintos y observa la identidad corrompida en la interfaz. - Reconcilia un array de números primitivos con
{ key: null }y explica por qué usarkeypor defecto ahí no tendría sentido.