wandres.dev
CONTROL FLOW: FOR E INDEX · listas keyed vs por índice

For: mapeo keyed por referencia

For mapea un array a nodos usando la referencia de cada objeto como clave de identidad. Cada item se renderiza una sola vez y su nodo DOM, junto con su ámbito reactivo y su estado, sigue a ese objeto por la lista aunque cambie de posición. Sin prop key, sin comparar índices: la identidad es la referencia en memoria.

⏱ 14 min

For es el componente de control flow que mapea una colección a DOM, y su rasgo definitorio es silencioso pero decisivo: es keyed por referencia. Cada objeto del array es su propia clave. El nodo que For fabrica para él —y el ámbito reactivo que lo envuelve— nace una sola vez y sigue a ese objeto por la lista, aunque salte de la posición 0 a la 9. No hay prop key, no hay comparación de índices: la identidad de una fila es la referencia de su objeto en el heap.

🎯 Al terminar esta lección sabrás
  • Entender que For usa la referencia del objeto, comparada con ===, como clave de identidad.
  • Ver que cada item se renderiza una vez y su nodo persiste mientras su referencia viva.
  • Comprender la firma del callback y por qué el item es un valor estático, no un accessor.
  • Situar cuándo For es la herramienta correcta frente a Index.

La referencia es la clave

Cuando el compilador ve <For each={tareas()}>, no genera un bucle que recorra el array en cada cambio. Genera una suscripción: For observa la expresión each y, cuando esta devuelve un array distinto, dispara su algoritmo de reconciliación. Ese algoritmo no compara texto ni posiciones; compara referencias de objeto con igualdad estricta. Para cada elemento del array nuevo formula una única pregunta —¿esta referencia ya existía en el array anterior?— y de la respuesta se derivan tres caminos: si existía, reutiliza el nodo y el ámbito que ya tenía; si es nueva, invoca el callback para fabricarlos; si una referencia del array viejo ya no aparece, desecha su nodo y ejecuta su limpieza.

import { For, createSignal } from "solid-js";

type Tarea = { id: number; texto: string };

function Lista() {
  const [tareas, setTareas] = createSignal<Tarea[]>([
    { id: 1, texto: "leer" },
    { id: 2, texto: "escribir" },
  ]);

  return (
    <ul>
      <For each={tareas()}>
        {(tarea) => <li>{tarea.texto}</li>}
      </For>
    </ul>
  );
}

De ahí el nombre keyed: la clave de cada fila no la pones tú con una prop, la aporta la propia identidad del objeto en memoria. Dos objetos con los mismos campos pero distinta referencia son, para For, dos filas distintas. El mismo objeto movido de sitio es, para For, la misma fila. Esta es la diferencia cruz con React, donde debes suministrar key a mano y donde olvidarla degrada el diff a una comparación por posición, frágil y propensa a que el estado salte de fila.

Cada item se renderiza una vez

La firma del callback es (item, index) => JSX, y cada parte encierra una decisión de diseño. El item es un valor estático, no un accessor: lo lees como tarea.texto, sin paréntesis. Puede permitírselo porque, por construcción, la referencia de una fila nunca cambia durante su vida; si el objeto fuese reemplazado por otro, sería una fila distinta con su propio nodo. El index, en cambio, es un accessor —lo llamas index()— porque la posición sí puede variar cuando la lista se reordena, y debe seguir siendo reactiva sin recrear la fila. Esa asimetría la disecciona 16.4.

<For each={tareas()}>
  {(tarea, indice) => {
    // este cuerpo corre UNA vez por cada referencia nueva
    const [abierta, setAbierta] = createSignal(false);
    return (
      <li onClick={() => setAbierta(!abierta())}>
        {indice()}. {tarea.texto} {abierta() ? "▼" : "▶"}
      </li>
    );
  }}
</For>

La consecuencia más valiosa de “una vez por referencia” es que cada fila puede alojar estado local que sobrevive a los reordenamientos. El signal abierta pertenece al ámbito de esa fila; cuando For mueve el nodo, mueve con él su ámbito entero —sus signals, sus memos, sus efectos, la posición de scroll, el foco de un input—. Nada de eso se reinicia, porque nada se recrea. En React ese estado vive fuera de la lista o depende de que la key sea perfecta; aquí cuelga físicamente del nodo que viaja.

Seguir al objeto aunque se mueva

Imagina la lista [A, B, C] y que el usuario la reordena a [C, A, B]. For no vuelve a ejecutar tres callbacks ni reconstruye tres li. Recorre el array nuevo, reconoce las tres referencias del anterior y emite un plan mínimo de operaciones del DOM: mover, no recrear. Los tres nodos siguen siendo los mismos objetos del DOM, con su estado intacto, reubicados con insertBefore.

flowchart LR
A[array nuevo] --> D[diff por referencia contra array previo]
D -->|misma referencia| M[mover nodo existente y su scope]
D -->|referencia nueva| N[crear nodo y scope una sola vez]
D -->|referencia ausente| X[desechar nodo y ejecutar cleanup]
M --> R[DOM reordenado sin recrear nada]
style M fill:#a6e3a1,color:#11111b
style N fill:#89b4fa,color:#11111b
style X fill:#f38ba8,color:#11111b

Solo tres eventos tocan el DOM de verdad: una referencia nueva crea, una referencia ausente desecha, y todo lo demás se mueve. Las filas cuya referencia persiste jamás re-ejecutan su callback: su coste de actualización es cero. Por eso For escala con el delta de la lista, no con su tamaño.

📝
No confundas identidad con igualdad de campos

For compara referencias, no contenidos. Si tu backend devuelve en cada fetch un array de objetos nuevos aunque describan las mismas entidades, For los verá como filas distintas y recreará todo. La cura no es un truco de For, es preservar identidad en la capa de datos: usa un createStore con reconcile, o mapea por id a objetos estables, para que la misma entidad conserve su referencia entre actualizaciones.

La firma completa: fallback y ámbito de fila

For es más que un mapeo. Su prop fallback se renderiza cuando el array está vacío, y For trata null o undefined en each como lista vacía sin lanzar errores. Eso jubila el ternario lista.length ? ... : ... que tanto ensucia el JSX heredado de React:

<For each={tareas()} fallback={<p>No hay tareas todavía.</p>}>
  {(tarea) => <li>{tarea.texto}</li>}
</For>

Bajo el callback late una segunda capa. Su cuerpo se ejecuta dentro de un owner: el ámbito reactivo que Solid usa para rastrear pertenencia y limpieza. Todo lo que crees ahí —un createEffect, un createMemo, un onCleanup, una suscripción— pertenece a esa fila. Mientras la referencia del objeto viva en el array, su owner vive; cuando desaparece, For lo destruye y ejecuta sus limpiezas de abajo arriba. Una fila no es un nodo suelto: es un subárbol reactivo con ciclo de vida propio, atado a la identidad de su objeto.

<For each={conexiones()}>
  {(con) => {
    const socket = abrir(con.url);      // se abre UNA vez, al crear la fila
    onCleanup(() => socket.cerrar());   // se cierra al salir del array
    return <li>{con.nombre}</li>;
  }}
</For>

Cuando con deja el array, For no solo retira el li: ejecuta su onCleanup y cierra el socket. Esa correspondencia entre la identidad del dato y el ciclo de vida del recurso convierte a For en una herramienta de gestión de recursos, no solo de presentación.

🌱

Referencia nueva

Aparece un objeto que no estaba: For invoca el callback una vez, crea su nodo y su owner, y lo inserta en su posición.

🔀

Referencia que persiste

El objeto ya existía: For reutiliza su nodo y su owner intactos y, si cambió de sitio, solo lo reubica. Cero re-ejecución.

🍂

Referencia ausente

El objeto sale del array: For retira su nodo y destruye su owner, ejecutando en orden los onCleanup de esa fila.

💡
For no es solo para el DOM

Como cada fila abre un owner, For materializa cualquier colección con recursos por elemento: conexiones, temporizadores, observadores. La identidad por referencia garantiza que abrir y cerrar ocurra exactamente una vez por entidad, sin fugas cuando la lista cambia. Piensa en For como un map que además gestiona el ciclo de vida de lo que crea.

ℹ️
Keyed no significa que declares la clave

En la jerga de otros frameworks, keyed evoca una prop que el programador escribe. En Solid describe el mecanismo, no una obligación tuya: la clave la deriva For de la referencia. No hay nada que teclear ni que mantener sincronizado; solo debes garantizar, en la capa de datos, que la misma entidad conserve su objeto entre actualizaciones.

📝
For envuelve el primitivo mapArray

For no es magia del compilador: es un componente que envuelve mapArray, el primitivo reactivo de Solid que mantiene la correspondencia entre referencias y resultados cacheados. Puedes usar mapArray directamente para mapear un array a valores no visuales conservando identidad por referencia. For es su encarnación en JSX, con fallback y ergonomía de componente.

La identidad no se declara, se posee

El salto mental que separa For de cualquier lista de React es este: en React la identidad de una fila es un dato que tú declaras —la prop key— y que el reconciliador cree bajo palabra; si mientes o la omites, el diff se equivoca en silencio y el estado salta de fila. En Solid la identidad de una fila es la referencia del objeto que la generó, un hecho físico del heap que no puedes falsear ni olvidar. For no te pregunta cuál es la clave porque ya la tiene: es el puntero. Esto reordena toda tu intuición sobre listas. Dejas de pensar “qué key pongo para que el framework no se confunda” y empiezas a pensar “qué objeto representa esta fila”, que es la pregunta correcta desde el principio. Cuando el modelo de datos usa objetos estables —filas de una base de datos, entidades con id, nodos de un árbol— For los sigue sin esfuerzo por reordenamientos, inserciones y borrados, preservando cada nodo del DOM y todo el estado que cuelga de él. La lista deja de ser algo que repintas y pasa a ser algo que se mantiene sincronizado por identidad. Ese es el regalo del mapeo keyed: no gestionas la correspondencia entre datos y DOM, la posees gratis.

⚔️ Comprueba que la fila sigue al objeto
  1. Crea un For sobre un array de objetos { id, texto } y, dentro de cada fila, un signal abierta que alterne con un clic.
  2. Abre la fila de en medio y reordena el array llevando ese objeto al principio. Confirma que sigue abierta y que su nodo no parpadeó.
  3. Pon un console.log en el cuerpo del callback y verifica que solo se imprime al insertar referencias nuevas, nunca al reordenar.
  4. Reemplaza un objeto por otro con los mismos campos pero distinta referencia; observa que esa fila se recrea —nuevo log, estado perdido— y explica por qué.
  5. Explica en una sola frase por qué For no necesita una prop key como React.
  6. Abre un WebSocket o un setInterval dentro del callback con su onCleanup; elimina ese objeto del array y confirma en consola que el recurso se cierra exactamente una vez.