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

El índice reactivo en For y el valor reactivo en Index

For e Index tienen firmas de callback que son imágenes especulares: For entrega valor estático e índice reactivo; Index entrega valor reactivo e índice estático. La asimetría no es arbitraria, cae directamente de sobre qué clavea cada uno. Aquí se explica la dualidad y se aplica el índice-signal y el valor-signal en casos reales.

⏱ 15 min

For e Index reciben ambos un callback (item, index), pero lo que entregan está cruzado. En For, el item es un valor estático y el index un accessor reactivo. En Index, el item es un accessor reactivo y el index un número estático. Exactamente al revés. Esa simetría no es un capricho de la API: es la consecuencia lógica de sobre qué clavea cada componente. Lo reactivo es siempre lo que puede cambiar mientras la fila sigue siendo la misma, y eso es distinto según claves por referencia o por posición.

🎯 Al terminar esta lección sabrás
  • Comprender por qué en For el índice es un accessor y el valor estático.
  • Comprender por qué en Index el valor es un accessor y el índice estático.
  • Ver la dualidad como consecuencia directa de la clave de cada componente.
  • Aplicar el índice-signal de For y el valor-signal de Index en casos prácticos.

La dualidad exacta

Parte del principio único: lo reactivo es lo que varía mientras la fila permanece. En For, la fila está atada a una referencia de objeto; esa referencia no cambia en toda la vida de la fila —si cambiara, sería otra fila—, así que el item puede ser estático. Lo que sí puede cambiar sin destruir la fila es su posición, cuando la lista se reordena; por eso el index es un accessor. En Index ocurre lo simétrico: la fila está atada a una posición fija, así que el index es un número estático, y lo que cambia bajo esa posición es el valor, de ahí que el item sea un accessor.

// For: valor estatico, indice reactivo
<For each={items()}>
  {(item, indice) => <li>{indice()}: {item.nombre}</li>}
</For>

// Index: valor reactivo, indice estatico
<Index each={items()}>
  {(item, indice) => <li>{indice}: {item().nombre}</li>}
</Index>
flowchart TD
F[For clavea por referencia] --> FV[valor estatico se lee item]
F --> FI[indice reactivo se llama indice]
X[Index clavea por posicion] --> XV[valor reactivo se llama item]
X --> XI[indice estatico numero fijo]
FI --> RE[siempre es reactivo lo que puede variar]
XV --> RE
style FI fill:#89b4fa,color:#11111b
style XV fill:#89b4fa,color:#11111b
style RE fill:#cba6f7,color:#11111b

Memoriza la regla y olvida la tabla: en cada componente, lo que se llama con paréntesis es lo variable, y lo que se lee sin paréntesis es el invariante. For invariante en el objeto, variable en la posición; Index invariante en la posición, variable en el valor.

El índice-signal de For en la práctica

Que el índice de For sea reactivo no es un detalle académico: es lo que permite que una numeración, un rayado cebra o una insignia de “primero” sigan siendo correctos tras un reordenamiento sin recrear filas. Cuando mueves un objeto, su nodo se reubica y su index() se recomputa solo; las clases y los números que dependen de él se actualizan de grano fino.

<For each={jugadores()}>
  {(jugador, posicion) => (
    <li classList={{ lider: posicion() === 0 }}>
      #{posicion() + 1}{jugador.nombre}
    </li>
  )}
</For>

Reordena la lista y la clase lider salta a la nueva primera fila, y el número #n de cada fila se recalcula, todo sin desmontar ni un solo li. El estado local de cada fila permanece, porque el nodo solo se movió; únicamente el binding que lee posicion() reacciona.

⚠️
El índice de For no es un identificador

index() es la posición de presentación, no la identidad. Cambia con cada reordenamiento, así que jamás lo uses como clave estable, como id de una petición, ni como dependencia que asuma permanencia. Para identidad usa item.id, que sí es estable; reserva index() para lo que de verdad depende del lugar: numerar, alternar color, marcar extremos.

El valor-signal de Index en la práctica

En Index el accessor es el item, y ese es justo el mecanismo que hace de Index la herramienta de los campos editables. El nodo del input se ancla a la posición; cuando el dato de esa posición cambia —lo escribas tú o llegue del servidor—, fluye por item() y solo el binding del value reacciona, sin tocar el elemento. El foco, el cursor y la selección se quedan donde el usuario los dejó.

<Index each={campos()}>
  {(campo, i) => (
    <input
      value={campo().valor}
      onInput={(e) => actualizar(i, e.currentTarget.value)}
    />
  )}
</Index>

Observa la división de trabajo: campo() es reactivo y alimenta el value; i es un número estático perfecto para localizar qué posición actualizar en el manejador, porque para ese input la posición nunca cambia. Con For esta misma escena se rompería —al cambiar el valor cambiaría la clave del primitivo y el input se remontaría—, y con Index encaja sin fricción.

ℹ️
Leer sin llamar, o llamar sin necesidad

Los dos errores de firma nacen de olvidar la dualidad. En For, escribir item() lanza un error en runtime porque item no es una función: es el objeto. En Index, escribir item.nombre sin paréntesis te entrega la función accessor, no el valor, y accedes a una propiedad que no existe o que no reacciona. La disciplina es mecánica: en For llamas el índice, en Index llamas el item; el otro miembro se lee tal cual.

Combinar los dos signals en una fila

La potencia real aparece cuando una fila deriva de su miembro reactivo. En For, combinas el índice-signal con el objeto estático para producir vistas que reaccionan solo a lo que cambia: el id no reacciona nunca, la posición sí.

<For each={pistas()}>
  {(pista, i) => {
    const par = () => i() % 2 === 0;          // deriva del indice-signal
    return (
      <li classList={{ par: par(), impar: !par() }}>
        {i() + 1}. {pista.titulo}
      </li>
    );
  }}
</For>

La derivación par se recomputa solo cuando i() cambia, es decir, cuando esa fila se reordena; el título, estático, no participa. En Index la derivación es simétrica: parte del valor-signal, y el índice es una constante que puedes capturar sin miedo en un closure porque no cambiará para esa fila.

<Index each={temperaturas()}>
  {(t, i) => {
    const alerta = () => t() > 38;            // deriva del valor-signal
    return <li classList={{ alerta: alerta() }}>sensor {i}: {t()}</li>;
  }}
</Index>

Aquí i entra en el closure como número literal —seguro, porque la posición es el invariante— mientras t() es la fuente reactiva que dispara alerta. Este patrón de derivar con una función que lee el miembro reactivo es el idioma que sustituye a useMemo: no declaras dependencias, la derivación se suscribe sola a lo que lee, sea i() o t(), y se recomputa solo cuando eso cambia. La precisión es automática.

Cuando la derivación es cara, promuévela a un createMemo explícito; hereda la misma suscripción fina al miembro reactivo:

<For each={filas()}>
  {(fila, i) => {
    const etiqueta = createMemo(() => `${i() + 1} de ${total()}`);
    return <li>{etiqueta()}</li>;
  }}
</For>
💡
Capturar el miembro estático en un closure es seguro

El miembro no reactivo de cada firma —el objeto en For, el índice en Index— es estable durante toda la vida de la fila, así que capturarlo en un manejador de eventos o en un setTimeout no congela nada indebido. Es el complemento reactivo el que debes leer siempre a través de su accessor. Confundirlos es el origen de la mitad de los bugs de firma que verás en 16.5.

ℹ️
El índice-signal de For tiene un coste que Index no paga

Que el índice de For sea reactivo no es gratis: For mantiene un signal de posición por fila y lo actualiza en los reordenamientos. Si tu lista nunca reordena y solo te interesa el número, ese signal es peso muerto, y el índice constante de Index es más ligero para numeraciones estáticas. Otro recordatorio de que la elección se paga en detalles, no solo en la foto grande.

📝
La regla en cinco palabras

Llama al que puede cambiar. En For, lo que cambia con la fila fija es la posición: llamas indice(). En Index, lo que cambia es el valor: llamas item(). El invariante —el objeto en For, el índice en Index— se lee tal cual, sin paréntesis. No hay nada más que memorizar.

Lo reactivo es el complemento del invariante

Hay un principio que gobierna no solo estas dos firmas sino toda la reactividad de grano fino: se hace reactivo aquello que puede cambiar mientras la entidad que lo contiene permanece. Un signal existe porque su celda de estado cambia mientras el componente que lo declaró sigue vivo. Una prop es un getter porque su valor cambia mientras la instancia hija persiste. Y aquí, el índice de For es un accessor porque la posición cambia mientras el objeto persiste, y el valor de Index es un accessor porque el dato cambia mientras la posición persiste. En cada caso, lo reactivo es el complemento del invariante: aquello que la clave deja libre para variar. Por eso las dos firmas están cruzadas y por eso no hay que memorizarlas por separado: son la misma ley aplicada a dos invariantes distintos. Cuando ves la API desde esta altura, dejas de preguntarte “¿este lleva paréntesis?” y lo deduces al instante: identifica qué fija la clave, y todo lo demás —lo que puede moverse sin romper la fila— es reactivo. Ese hábito de razonar desde el invariante es lo que convierte la reactividad de Solid de un conjunto de reglas sueltas en un sistema que se predice solo.

⚔️ Usa el índice-signal y el valor-signal
  1. Con For, muestra #{posicion() + 1} en cada fila y marca la primera con una clase; reordena y confirma que el número y la clase se recolocan sin recrear filas.
  2. Intenta usar index() de For como si fuera un id estable para una acción; provoca el fallo reordenando y explica por qué es incorrecto.
  3. Con Index, ata cada fila a un <input> cuyo value sea campo().valor y actualiza por código otra fila; verifica que no pierdes el foco.
  4. Fuerza los dos errores de firma —item() en For e item.x sin paréntesis en Index— y describe el fallo de cada uno.
  5. Enuncia, en una sola regla, cómo deducir qué miembro del callback es reactivo en cualquiera de los dos componentes.
  6. Escribe una derivación por fila con createMemo que dependa del índice-signal en For y comprueba que solo se recomputa al reordenar, no al cambiar datos ajenos.