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.
Guardas un objeto o un array en un signal, lo modificas, y la interfaz no reacciona. No hay error, no hay aviso: simplemente nada se mueve. Es el bug más desconcertante de quien llega a Solid desde modelos que reaccionan a la mutación, y su raíz es una sola frase que hay que interiorizar hasta que duela: el signal compara referencias, no contenido, y mutar en sitio no cambia la referencia.
- Entender por qué
===sobre objetos compara la referencia y no el contenido. - Reconocer la mutación en sitio como un cambio que el grafo no llega a ver.
- Aplicar la disciplina de reemplazo con
spreadpara arrays y objetos. - Saber cuándo el reemplazo inmutable estorba y conviene un
store.
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.
const [tareas, setTareas] = createSignal([{ id: 1, hecha: false }]);
// MAL: muta en sitio, misma referencia, no notifica
const t = tareas();
t.push({ id: 2, hecha: false });
setTareas(t); // === dice que es el mismo array -> nadie se entera
// BIEN: referencia nueva
setTareas((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.
flowchart LR M[muta en sitio push splice sort] --> R1[misma referencia] --> N1[compara igual, no notifica] P[reemplaza con spread o map] --> R2[referencia nueva] --> N2[compara distinto, notifica] style M fill:#89b4fa,color:#11111b style P fill:#89b4fa,color:#11111b style N1 fill:#f38ba8,color:#11111b style N2 fill:#a6e3a1,color:#11111b
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.
// Arrays: cada operacion produce una referencia fresca
setTareas((prev) => prev.filter((t) => t.id !== 2)); // borrar
setTareas((prev) => prev.map((t) => t.id === 1 ? { ...t, hecha: true } : t)); // editar
setTareas((prev) => [...prev, nueva]); // anadir
// Objetos: spread y sobrescribe
const [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.
Mutan en sitio
push, pop, splice, sort, reverse, fill. Conservan la referencia: tras ellas, el set no notifica.
Devuelven una copia
map, filter, concat, slice, toSorted, with. Producen un array nuevo: referencia fresca que sí notifica.
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:
const [ui, setUi] = createSignal({
tema: "oscuro",
editor: { fuente: "mono", tamano: 14 },
});
// Reconstruir cada nivel tocado, o mutas por accidente el objeto compartido
setUi((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ó.
La disciplina inmutable no exige duplicar la estructura entera en cada escritura. El spread practica compartición estructural: reconstruyes solo los nodos del camino que va de la raíz al campo tocado, y todas las ramas intactas se comparten por referencia con el estado anterior. Cambiar una hoja a tres niveles de profundidad crea tres objetos nuevos, no un clon completo. Ese es el mismo principio que sostiene a las estructuras persistentes: identidad nueva donde hubo cambio, referencia compartida donde no. Lo que duele no es la memoria, es la verbosidad de escribir el camino a mano, y eso es exactamente lo que el store automatiza.
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.
import { createStore, produce } from "solid-js/store";
const [estado, setEstado] = createStore({
tema: "oscuro",
editor: { fuente: "mono", tamano: 14 },
});
// Escritura por ruta: granular, sin reconstruir niveles
setEstado("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.
El patrón que más horas roba es tomar el objeto del signal, mutarlo y reestablecerlo esperando una actualización. Solid no da error porque, desde su punto de vista, no hiciste nada: le ofreciste el mismo objeto que ya tenía. Si una vista no reacciona a un cambio que estás seguro de haber hecho, sospecha lo primero de una mutación en sitio seguida de un set con la misma referencia. La prueba definitiva: añade { ...obj } o cambia el método mutador por su versión que copia, y observa si de golpe reacciona.
En Solid, la inmutabilidad no es una preferencia estilística heredada de la programación funcional ni una moda que puedas ignorar sin consecuencias: es el mecanismo mismo por el que el cambio se vuelve visible para el grafo. La reactividad de grano fino necesita una forma barata y fiable de decidir si un valor cambió, y la más barata que existe es comparar referencias; pero comparar referencias solo funciona si te comprometes a que una referencia nueva signifique siempre un contenido nuevo, y una referencia repetida signifique siempre contenido idéntico. Mutar en sitio rompe ese pacto: cambias el contenido pero conservas la referencia, y le mientes al sistema diciéndole que nada pasó. Por eso la disciplina inmutable no es una carga que Solid te impone por purismo, sino la contraparte lógica de haber elegido la comparación más eficiente posible. Y por eso, cuando reconstruir estructuras profundas se vuelve absurdo, la respuesta no es abandonar la inmutabilidad y empezar a mutar, sino cambiar de primitivo: el store te devuelve la comodidad de escribir como si mutaras, mientras por debajo mantiene el mismo contrato de identidad que hace observable cada cambio. Signal para valores que reemplazas enteros, store para árboles que editas por partes: en ambos, el principio es el mismo —el cambio solo existe si el sistema puede verlo.
- Guarda un array en un signal, hazle
pushy reestablécelo con la misma referencia; confirma que la vista no reacciona. - Corrígelo con
[...prev, x]dentro de un actualizador funcional y verifica que ahora sí notifica. - Ordena la lista con
sorty reestablécela; observa que no cambia nada. Cámbialo portoSortedy comprueba la diferencia. - Modela un estado con tres niveles de anidamiento y edita el campo más hondo con
spread; cuenta cuántos niveles tienes que reconstruir. - Reescribe ese mismo estado con
createStorey edítalo por ruta; razona qué lectores se despiertan en un caso y en el otro.