Arrays en el store: índice, predicado y el patrón de listas
El store convierte cada índice de un array en una hoja direccionable: editas por posición, por lista de índices, por rango o por un predicado funcional que selecciona los elementos a tocar. Añadir, filtrar y reordenar viven en un actualizador que devuelve un array nuevo conservando identidad. Con For keyed, editar campos no remonta filas: ese es el patrón de listas de Solid.
Una lista es el estado más común y el más fácil de reaccionar mal. En un signal, cualquier edición te obligaba a reconstruir el array entero y arriesgabas remontar cada fila. El store cambia las reglas: trata cada índice como una propiedad direccionable, de modo que editar el campo de un elemento es tan quirúrgico como editar cualquier hoja, y solo esa celda del DOM se actualiza. A eso se suma un vocabulario de selección —índice, lista de índices, rango, predicado— que te deja alcanzar los elementos correctos sin recorrer a mano. Dominar esto es dominar la reactividad de listas de Solid.
- Editar un elemento por índice y varios por lista de índices o por rango.
- Seleccionar elementos con un predicado funcional en la ruta.
- Añadir, quitar y reordenar con un actualizador que devuelve un array nuevo.
- Aplicar el patrón de listas con
Forkeyed para no remontar filas al editar.
El índice es una hoja más
Dentro de una ruta, un número es una clave: navega a esa posición del array igual que un string navega a la clave de un objeto. Editar el campo de un elemento concreto es, por tanto, alargar la ruta hasta él.
import { createStore } from "solid-js/store";
const [estado, setEstado] = createStore({
tareas: [
{ id: 1, texto: "leer", hecha: false },
{ id: 2, texto: "escribir", hecha: false },
],
});
setEstado("tareas", 0, "hecha", true); // elemento 0, campo hecha
setEstado("tareas", 1, "texto", (t) => t + "!"); // actualizador en la hoja
Fíjate en lo que no ocurre: no reconstruyes el array, no reemplazas el objeto de la tarea, no tocas al elemento vecino. Solid escribe la hoja tareas[0].hecha y notifica solo a quien la lee. Si esa tarea es una fila renderizada, únicamente su casilla se actualiza; el resto de la lista ni se inmuta. Esta es la diferencia abismal con reemplazar el array entero: la edición de un campo es local de verdad.
Y la ruta no se detiene en el primer nivel del elemento: sigue tan hondo como haga falta, igual que en un objeto anidado. Un campo dentro de un subobjeto de un elemento de la lista es una hoja como cualquier otra.
// Numero y string se alternan en la ruta segun navegues array u objeto
setEstado("tareas", 0, "meta", "prioridad", "alta");
La regla mental es uniforme: en la ruta, un número significa «entra en esta posición del array» y un string significa «entra en esta clave del objeto». Alternándolos describes cualquier trayecto por el árbol, sin importar cuántas listas y cuántos objetos se crucen por el camino.
Seleccionar varios: índices, rangos y predicados
Rara vez conoces el índice exacto; conoces una condición. La ruta del store acepta, en la posición donde iría un índice, tres formas de selección múltiple que aplican el resto de la ruta a cada elemento elegido.
// Lista explicita de indices
setEstado("tareas", [0, 2, 4], "hecha", true);
// Rango: desde, hasta, cada cuanto
setEstado("tareas", { from: 0, to: 3 }, "hecha", true);
// Predicado: la funcion decide que elementos entran
setEstado("tareas", (t) => !t.hecha, "hecha", true); // marca las pendientes
setEstado("tareas", (t) => t.id === 2, "texto", "revisar"); // por identidad de negocio
El predicado es la joya: recibe cada elemento y su índice, y donde devuelve verdadero, Solid aplica el resto de la ruta. Es un where declarativo incrustado en la escritura, que te libera de encontrar el índice a mano con un findIndex previo. Selecciona por la propiedad de negocio —el id, un estado, una fecha— y deja que el store recorra y edite en tu lugar, notificando solo a las filas que de verdad cambiaron.
flowchart TD
A[setStore tareas selector campo valor] --> B{que es el selector}
B -->|numero| C[un indice concreto]
B -->|array de numeros| D[varios indices]
B -->|objeto from to by| E[un rango]
B -->|funcion predicado| F[los que cumplen la condicion]
C --> G[aplica el resto de la ruta a cada elegido]
D --> G
E --> G
F --> G
style F fill:#cba6f7,color:#11111b
style G fill:#a6e3a1,color:#11111bAñadir, quitar, reordenar: cambia la forma del array
Editar campos toca hojas internas, pero cambiar la longitud o el orden cambia el array como colección, y eso vive en un actualizador que devuelve un array nuevo. Como devolver un array reemplaza la referencia —no se fusiona—, construyes el contenido completo del siguiente con las operaciones que producen copia.
setEstado("tareas", (t) => [...t, nueva]); // anadir al final
setEstado("tareas", (t) => t.filter((x) => x.id !== 2)); // quitar
setEstado("tareas", (t) => [...t].sort(cmp)); // reordenar
Hay un idioma de anexado que aprovecha la ruta: escribir en el índice igual a la longitud crea la posición nueva. setEstado("tareas", estado.tareas.length, nueva) empuja sin reconstruir el array, útil cuando solo añades. Para quitar y reordenar, en cambio, el actualizador que devuelve la copia filtrada u ordenada es lo natural, porque la forma entera de la colección cambia.
Devolver un array nuevo asusta si vienes creyendo que reemplazar la colección remonta todo. No lo hace, siempre que conserves las referencias de los elementos. Al esparcir [...t, nueva], los objetos existentes mantienen su identidad; For reconoce que son los mismos y solo añade la fila nueva. El remonte masivo solo ocurre si sustituyes los elementos por objetos frescos con el mismo contenido, y para ese caso —típico de datos del servidor— existe reconcile, que verás en la lección 5.
Los métodos que mutan en sitio —sort, reverse, splice— no pueden correr sobre estado.tareas directamente: el proxy del store es de solo lectura y lo rechazará. Por eso el actualizador esparce primero, [...t].sort(cmp), ordenando una copia fresca que luego reemplaza la referencia. La misma cautela vale al entregar el array a código externo que pudiera mutarlo: pásale unwrap(estado.tareas) si necesita el objeto crudo, y trata su resultado como un valor nuevo que vuelve al store por el setter, jamás como una mutación in situ.
El patrón de listas
Junta las piezas y emerge el patrón que Solid recomienda para toda lista editable. El array vive en un store; el render usa For, que es keyed por referencia de cada elemento; los campos se editan por índice o por predicado, sin tocar la colección; y la forma —añadir, quitar, ordenar— se cambia con un actualizador que preserva identidades.
import { For } from "solid-js";
<For each={estado.tareas}>
{(tarea, i) => (
<li>
<input
type="checkbox"
checked={tarea.hecha}
onChange={() => setEstado("tareas", i(), "hecha", (h) => !h)}
/>
{tarea.texto}
</li>
)}
</For>
El resultado es reactividad de grano fino a nivel de fila: marcar una casilla escribe una hoja y actualiza un solo nodo del DOM, sin recrear la lista ni recorrer a los hermanos. For mantiene cada fila viva mientras su elemento conserve la referencia, así que las ediciones de campo son gratis en términos de reconciliación y solo las verdaderas altas y bajas mueven nodos. Es la culminación de todo el nivel: la lista, que era la estructura más propensa a reaccionar en bloque, se vuelve el ejemplo más puro de grano fino.
Las lecturas derivadas encajan en el mismo patrón sin fricción. Un recuento de pendientes, un total, un filtrado para mostrar solo lo activo: todos se expresan como memos que leen el array del store, y como el store rastrea por hoja, ese memo solo se recalcula cuando cambia algo que de verdad lee.
import { createMemo } from "solid-js";
const pendientes = createMemo(() => estado.tareas.filter((t) => !t.hecha).length);
Marcar una tarea como hecha recalcula pendientes porque toca el campo que el memo observa; editar el texto de otra tarea, en cambio, lo deja intacto. La derivación hereda la granularidad del store: reacciona a la dimensión exacta del cambio, no a que «la lista cambió» en abstracto.
En For, el segundo parámetro es un accessor de señal, no un entero fijo: se escribe i(), con paréntesis, porque el índice de una fila puede cambiar si la lista se reordena. Pasar i sin invocar, o capturar su valor una vez en una variable, te dejará escribiendo en la posición equivocada tras cualquier reordenación. Cuando tu criterio es la identidad de negocio y no la posición, prefiere el predicado (t) => t.id === tarea.id, inmune al orden.
La dificultad histórica de las listas reactivas nace de tratar la colección como una sola cosa indivisible: si el array es un único valor, entonces tocar el campo de un elemento y añadir un elemento nuevo son, para el sistema, el mismo tipo de suceso —el array cambió— y ambos arrastran la misma penalización de reevaluar la lista entera. El store rompe esa igualdad falsa distinguiendo dos ejes que siempre estuvieron mezclados. El primero es el eje del contenido: los campos internos de los elementos, cada uno una hoja direccionable con su propio signal perezoso, que editas por índice o por predicado sin que la colección como tal se entere, porque desde su punto de vista ningún elemento entró ni salió, solo cambió el interior de uno que ya estaba. El segundo es el eje de la forma: la longitud y el orden de la colección, que sí constituyen un cambio del array como estructura y por eso viven en un actualizador que devuelve una nueva secuencia. Separar estos dos ejes es lo que permite que editar mil casillas de una tabla no cueste como reconstruir la tabla mil veces, sino como mil escrituras de hoja independientes, cada una despertando una sola fila. Y el predicado en la ruta es la expresión más refinada de esta idea: te deja hablar el idioma del dominio —«todas las tareas vencidas», «el usuario con este id»— en lugar del idioma de las posiciones, de modo que tu escritura describe qué cambia semánticamente y el store resuelve dónde está eso en la estructura y a quién hay que avisar. Cuando esto se asienta, el patrón de listas deja de ser una receta que memorizas y se vuelve una consecuencia obvia: contenido por hoja, forma por actualizador, identidad por referencia, y For cosiendo todo con reconciliación mínima. La lista, que empezó siendo la pesadilla de la reactividad, termina siendo su demostración más elegante.
- Marca
hechaen un elemento por índice y confirma en devtools que solo esa fila se actualiza, no la lista entera. - Usa un predicado para marcar todas las tareas pendientes de una vez y comprueba que solo las que cambian notifican.
- Añade un elemento con
[...t, nueva]y verifica que las filas existentes conservan su identidad y no remontan. - Filtra para eliminar un elemento y observa que solo desaparece su nodo, sin recrear los vecinos.
- Renderiza con
Forusandoi()y luego con un predicado porid; reordena la lista y razona por qué el predicado es inmune al cambio de posición.