Saltar al contenido

Objetos y arrays en un signal: no mutes en sitio

Un signal compara con ===, que mira la referencia y no el contenido. Mutar un objeto o un array en su sitio conserva la referencia, así que Solid concluye que nada cambió y no notifica. La disciplina es reemplazar por una referencia nueva; cuando reconstruir duele, el store entra en escena.

La igualdad por referencia y la mutación invisible

El equals por defecto de un signal es ===. Sobre primitivas compara valores; sobre objetos y arrays compara identidad de referencia: dos variables son iguales solo si apuntan al mismo objeto en memoria. La consecuencia es implacable: si tomas el objeto que hay dentro del signal, lo mutas y lo vuelves a establecer, le estás pasando a Solid la misma referencia, que por definición es igual a sí misma. El sistema concluye que no ha cambiado nada y no notifica.

TSX
const [tareas, setTareas] = createSignal([{ id: 1, hecha: false }]);// MAL: muta en sitio, misma referencia, no notificaconst t = tareas();t.push({ id: 2, hecha: false });setTareas(t); // === dice que es el mismo array -> nadie se entera// BIEN: referencia nuevasetTareas((prev) => [...prev, { id: 2, hecha: false }]);

Los métodos mutadores de array son la trampa más frecuente porque su nombre no delata el peligro: push, splice, sort, reverse, fill modifican el array en su lugar y devuelven —o el mismo array, o una longitud— sin crear nada nuevo. Ordenar la lista con arr.sort() y reestablecerla es un no-op reactivo perfecto: la ves ordenada en memoria pero la vista sigue mostrando el orden viejo.

Que Solid compare por referencia y no por contenido no es un descuido, es una elección deliberada de rendimiento. Comparar dos referencias es una única instrucción; comparar dos objetos por contenido es un recorrido cuyo coste crece con el tamaño del dato, y habría que pagarlo en cada escritura de cada signal. Al elegir la comparación más barata que existe, Solid mantiene la reactividad de grano fino a un coste ínfimo, y traslada la contraparte a tu código en forma de disciplina: comprométete a que una referencia nueva signifique contenido nuevo, y el sistema te creerá sin verificar.

Reemplazar: la disciplina inmutable

La cura es tratar el estado como inmutable: nunca modificar lo que hay, sino producir un valor nuevo y establecerlo. Para arrays, usa las operaciones que devuelven una copia en lugar de las que mutan; para objetos, esparce el anterior y sobrescribe las claves que cambian.

TSX
// Arrays: cada operacion produce una referencia frescasetTareas((prev) => prev.filter((t) => t.id !== 2));            // borrarsetTareas((prev) => prev.map((t) => t.id === 1 ? { ...t, hecha: true } : t)); // editarsetTareas((prev) => [...prev, nueva]);                          // anadir// Objetos: spread y sobrescribeconst [config, setConfig] = createSignal({ tema: "oscuro", zoom: 1 });setConfig((c) => ({ ...c, tema: "claro" }));

El actualizador funcional —la forma setSignal(prev => ...)— no es un capricho aquí: garantiza que partes del valor más reciente y deja explícito que estás derivando un nuevo estado del anterior. JavaScript moderno además te regala versiones no mutadoras de los clásicos: toSorted, toReversed, with y toSpliced devuelven un array nuevo sin tocar el original, y son la elección natural dentro de un set.

Interioriza el criterio para no dudar en caliente: si el nombre del método suena a orden que transforma algo —ordena, invierte, mete, saca— desconfía, porque casi siempre muta en su sitio; si suena a que produce un resultado —mapea, filtra, concatena— es seguro, porque devuelve una copia. Y ante cualquier duda, esparce con [...arr] u { ...obj } antes de operar: el coste de una copia superficial de más es despreciable frente al de un cambio que nunca se propaga. La regla operativa cabe en una frase: nunca escribas sobre lo que el signal ya contiene; construye algo nuevo y entrégaselo.

Anidamiento: el spread superficial no alcanza

El reemplazo inmutable tiene un coste que crece con la profundidad. El spread es superficial: { ...c } copia el primer nivel pero comparte por referencia los objetos anidados. Si quieres cambiar un campo hondo sin mutar, tienes que reconstruir cada nivel del camino hasta él:

TSX
const [ui, setUi] = createSignal({  tema: "oscuro",  editor: { fuente: "mono", tamano: 14 },});// Reconstruir cada nivel tocado, o mutas por accidente el objeto compartidosetUi((s) => ({ ...s, editor: { ...s.editor, tamano: 16 } }));

Con dos niveles aún es tolerable; con cinco, cada escritura se vuelve una escalera de spread frágil y ruidosa donde un nivel olvidado reintroduce la mutación compartida. Y hay un coste reactivo escondido: al reemplazar la raíz notificas a todos los que leen el signal, aunque solo cambiara una hoja. El signal es atómico: no sabe distinguir qué parte cambió.

El store como salida de grano fino

Cuando el estado es un árbol y no un valor, la herramienta deja de ser el signal y pasa a ser el store. createStore envuelve la estructura en un proxy que rastrea por propiedad: escribes por ruta, sin spread manual, y solo se enteran los lectores de la rama que tocaste.

TSX
import { createStore, produce } from "solid-js/store";const [estado, setEstado] = createStore({  tema: "oscuro",  editor: { fuente: "mono", tamano: 14 },});// Escritura por ruta: granular, sin reconstruir nivelessetEstado("editor", "tamano", 16);// Estilo mutable pero rastreado, via produce:setEstado(produce((s) => { s.editor.tamano = 16; }));

Fíjate en produce: te deja escribir con apariencia de mutación —s.editor.tamano = 16— pero por dentro es un proxy que registra cada cambio y notifica de forma granular. Es la conciliación entre la ergonomía de mutar y la corrección de la reactividad. La lección 5 de este nivel profundiza en cuándo cruzar del signal al store; por ahora quédate con la señal de alarma: si te sorprendes escalando spread sobre spread, el signal ya no es la granularidad correcta.

No leas mal el mensaje, sin embargo: el store no permite mutar, sino que hace rastreable una escritura con forma de mutación. La diferencia es total. Mutar el objeto de un signal por fuera sigue siendo un bug, exactamente igual que antes; lo que produce te da es un borrador proxy, vivo solo dentro de su función, donde cada asignación se traduce en una notificación granular por debajo. Sigues sin poder mutar el estado a tus espaldas del sistema; solo has cambiado la sintaxis con la que le declaras los cambios, del spread en escalera a la asignación directa vigilada.