Saltar al contenido

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.

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.

Sobre 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:

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

TSX
import { reconcile } from "solid-js/store";setState("filas", reconcile(frescas));                  // empareja por la propiedad idsetState("filas", reconcile(frescas, { key: "uuid" })); // empareja por uuidsetState("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.

TSX
// key equivocada: apunta a un campo que cambia -> todo parece nuevo y remontasetState("filas", reconcile(frescas, { key: "actualizado" }));// key correcta: apunta al identificador inmutable -> preserva lo que persistesetState("filas", reconcile(frescas, { key: "id" }));

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.

TSX
// Array de primitivos: la posición es la identidadsetState("puntuaciones", reconcile(nuevas, { key: null }));// Tupla de forma fija: cada ranura tiene un significado por su lugarsetState("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.

TSX
setState("filas", reconcile(frescas, { key: "id" }));   // identidad explícitasetState("valores", reconcile(nums, { key: null }));    // posicional, primitivossetState("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.