Arrays en stores: acceso, For y reactividad por índice
Un array dentro de un store es otra rama proxy: accedes con estado.items[i].campo, escribes por posición con setEstado items i campo valor, y añades por longitud o con un actualizador. Iterar con For conserva la identidad de cada elemento, así que editar un campo por ruta actualiza esa fila sin remontarla; mapear con objetos nuevos, en cambio, la remonta. La reactividad distingue longitud, posición y hoja.
Un array dentro de un store no es un caso especial: es otra rama del proxy, con la misma promesa de grano fino que cualquier objeto anidado. Pero los arrays traen preguntas propias que hay que responder con precisión: cómo accedes a un elemento y a su campo, cómo escribes por posición sin reconstruir el array, cómo iteras sin remontar filas en cada cambio, y qué significa exactamente reactividad por índice. Dominar estas cuatro cosas es la diferencia entre una lista que se actualiza con cirugía y una que se repinta entera a cada pulsación.
- Acceder a elementos y campos de un array del store por índice y por ruta.
- Escribir por posición, por rango, por predicado y añadir al final sin
spreadmanual. - Iterar con
<For>conservando la identidad de cada elemento entre cambios. - Distinguir la reactividad de la longitud, de la posición y de la hoja de cada elemento.
Arrays como ramas del store
El array vive dentro del proxy, así que cada elemento y cada campo de cada elemento es su propia hoja rastreada. Accedes como accederías a cualquier estructura, y la lectura suscribe con la misma finura: leer estado.items[1].hecha te ata a esa hoja, no al array entero.
import { createStore } from "solid-js/store";
const [estado, setEstado] = createStore({
items: [
{ id: 1, texto: "a", hecha: false },
{ id: 2, texto: "b", hecha: false },
],
});
estado.items[1].texto; // "b" — hoja concreta
estado.items.length; // 2 — la longitud también es reactiva
La escritura por ruta se extiende a los índices y ofrece formas potentes de tocar varios elementos a la vez sin recorrer el array a mano. Puedes escribir una posición concreta, un conjunto de índices, un rango o un predicado que selecciona los elementos a modificar.
// Por posición y campo
setEstado("items", 1, "hecha", true);
// Añadir al final: escribe en el índice igual a la longitud actual
setEstado("items", estado.items.length, { id: 3, texto: "c", hecha: false });
// Varios índices a la vez
setEstado("items", [0, 1], "hecha", true);
// Por predicado: selecciona qué elementos y escribe su campo
setEstado("items", (t) => !t.hecha, "hecha", true);
// Sobre el array entero, con un actualizador que produce referencia nueva
setEstado("items", (arr) => arr.filter((t) => t.id !== 1));
Las formas por índice, por lista de índices, por rango y por predicado son escrituras quirúrgicas: tocan solo las hojas seleccionadas y conservan intacto todo lo demás. El actualizador sobre el array entero es la excepción y merece cuidado, como veremos enseguida.
Añadir y quitar merecen una nota aparte, porque son cambios estructurales del array: tocan su longitud, no solo una hoja. Añadir escribe en el índice igual a la longitud actual; quitar y reordenar reemplazan el array con una referencia nueva mediante un actualizador que produce el array resultante.
// Quitar por posición: reemplazo estructural con referencia nueva
setEstado("items", (arr) => arr.filter((_, i) => i !== 1));
// Reordenar: For moverá las filas existentes en lugar de recrearlas
setEstado("items", (arr) => [arr[1], arr[0], ...arr.slice(2)]);
El anidamiento se comporta igual, sin límite de profundidad: si cada elemento tiene su propio array de subetiquetas, estado.items[i].subtags[j] es otra hoja rastreada, y setEstado("items", i, "subtags", j, valor) la escribe con la misma precisión. La reactividad de grano fino no se detiene en el primer array; recorre el árbol entero, arrays incluidos, hasta la hoja que nombres.
Iterar con For sobre un array del store
<For> es el componente de iteración correcto para arrays de objetos, y lo es precisamente por cómo se lleva con los stores. <For> empareja cada elemento con su fila por referencia: mientras la identidad de un elemento no cambie, su fila no se recrea. Y como el store conserva la identidad de cada elemento cuando editas un campo por ruta, editar un campo actualiza solo el binding afectado dentro de la fila, sin remontar el nodo.
import { For } from "solid-js";
<For each={estado.items}>
{(item) => (
<li classList={{ hecha: item.hecha }}>{item.texto}</li>
)}
</For>
Cuando ejecutas setEstado("items", 1, "hecha", true), el elemento de índice 1 sigue siendo el mismo proxy que antes: solo cambió su hoja hecha. <For> no ve un elemento nuevo, así que no toca el <li>; únicamente el binding de classList, suscrito a item.hecha, se reejecuta. Una lista de mil filas donde marcas una casilla actualiza exactamente una casilla.
flowchart TD AR[array items en el store] --> FOR[For rastrea longitud e identidad] FOR --> IT[cada elemento es un proxy con identidad estable] IT --> BI[bindings por hoja dentro de la fila] ED[setEstado items i campo valor] --> BI ED --> KEEP[la identidad del elemento no cambia asi que la fila no se remonta] style KEEP fill:#a6e3a1,color:#11111b style ED fill:#89b4fa,color:#11111b
setEstado("items", i, "hecha", true) cambia un campo dejando intacta la identidad del elemento, así que <For> actualiza esa fila sin remontarla. Pero setEstado("items", (arr) => arr.map((t) => ({ ...t, hecha: true }))) crea un objeto nuevo por cada elemento: <For>, que empareja por referencia, los ve como filas distintas y remonta el lote entero, perdiendo estado del DOM, foco y animaciones. La regla operativa: para editar campos, escribe por ruta; reserva el reemplazo del array para cambios estructurales —añadir, quitar, reordenar.
Reactividad por índice: longitud, posición y hoja
Aquí conviene separar tres suscripciones distintas que a menudo se confunden. Leer estado.items.length te suscribe a la longitud: añadir o quitar elementos te despierta, pero editar un campo de un elemento existente no. Leer estado.items[i] te suscribe a la posición i: te despierta si esa ranura pasa a apuntar a otro elemento. Leer estado.items[i].campo te suscribe a la hoja: te despierta solo si ese campo concreto cambia.
Esa distinción es la que separa a <For> de <Index>. <For> está pensado para listas de objetos con identidad estable: mapea por referencia, y por eso reordenar mueve filas sin recrearlas. <Index> mapea por posición: la fila número i es fija y lo que cambia es su contenido, que recibes como un accessor. <Index> es la elección correcta cuando el array es de primitivas —donde no hay identidad de referencia que preservar— o cuando la posición es la semántica, como en una fila de inputs fijos.
import { Index } from "solid-js";
// Index: la fila es la posición; el valor llega como accessor
<Index each={estado.items}>
{(item, i) => <li>{item().texto}</li>}
</Index>
Nota una asimetría reveladora en las firmas de ambos. En <For> el callback recibe el elemento como valor directo y el índice como un signal —(item, indice) => ...—, porque el elemento es estable y su posición puede cambiar. En <Index> es al revés: el índice es un número fijo y el valor llega como accessor —(item, indice) => item()—, porque la posición es estable y el contenido puede cambiar. La forma de cada firma te recuerda cuál de los dos ejes es el que permanece fijo.
Elegir mal entre <For> e <Index> no rompe la aplicación, pero degrada su comportamiento: <Index> sobre objetos con identidad reordena recreando contenido en vez de mover filas, y <For> sobre primitivas que se repiten trata valores iguales como colisiones de clave. La brújula: identidad estable y reordenamientos, <For>; posición fija o primitivas, <Index>.
For: por identidad
Empareja cada elemento con su fila por referencia. Reordenar mueve filas sin recrearlas; editar un campo por ruta actualiza la fila sin remontarla. Para objetos con identidad estable.
Index: por posición
La fila es el índice y el valor llega como accessor. Al cambiar, recrea el contenido en su sitio sin mover la fila. Para primitivas o cuando la posición es la semántica.
Un array en un store no es una lista que se repinta cuando cambia: es una estructura reactiva con tres granularidades simultáneas que hay que aprender a distinguir para razonar sobre ella con precisión. El primer eje es la longitud: quién lee length depende de cuántos elementos hay, y solo eso lo despierta. El segundo es la posición: quién lee la ranura i depende de qué elemento ocupa ese hueco, y <Index> construye su iteración sobre este eje, fijando la fila y haciendo señal el valor. El tercero es la hoja: quién lee items[i].campo depende de un dato concreto de un elemento concreto, y <For> construye su iteración conservando la identidad del elemento para que este eje funcione fila a fila sin remontajes. La consecuencia práctica es que la elección entre <For> e <Index> y la elección entre editar por ruta o reemplazar el array no son detalles de estilo: son decisiones sobre qué eje de reactividad quieres usar. Editar por ruta actúa sobre el eje de la hoja y deja la identidad quieta, así que <For> no remonta; reemplazar con map fabrica identidades nuevas y fuerza a <For> a tratar todo como recién llegado. Reordenar sin recrear exige identidad estable, y por eso <For> acierta donde <Index> recrea. Interiorizado esto, dejas de pelear con listas que parpadean o pierden el foco y empiezas a escribir mutaciones que tocan exactamente el eje —longitud, posición u hoja— que de verdad cambió.
- Modela una lista de tareas en un store y marca una como hecha con
setEstado("items", i, "hecha", true); con<For>, confirma que solo esa fila se actualiza y no se remonta. - Cambia esa edición por un
.mapque crea objetos nuevos y observa en las devtools cómo<For>remonta todas las filas. - Marca de golpe todas las tareas pendientes con un predicado:
setEstado("items", (t) => !t.hecha, "hecha", true). - Sustituye
<For>por<Index>sobre el mismo array y razona en qué casos cada uno es el correcto. - Lee
estado.items.lengthen un efecto y comprueba que añadir o quitar elementos lo dispara, pero editar un campo de un elemento existente no.