La potencia de createMutable: imperativo, getters y setters, interop
Donde createMutable brilla: código imperativo que se lee como JavaScript de toda la vida —state.items.push, state.n++, delete state.x—, getters que se vuelven derivaciones reactivas y setters que interceptan la asignación como en una clase, e interoperabilidad con APIs de terceros que esperan mutar un objeto plano. Por qué esa naturalidad es real y no un truco, y qué patrones desbloquea.
Si la lección anterior te dejó con la guardia alta, esta te muestra por qué el primitivo existe a pesar de sus riesgos. createMutable tiene una virtud que ningún otro estado de Solid iguala: se comporta como un objeto de JavaScript corriente. Puedes hacer push, incrementar con ++, borrar con delete, y todo sigue siendo reactivo. Puedes definir getters que se vuelven derivaciones y setters que interceptan la asignación, igual que en una clase. Y puedes entregarle el proxy a una librería de terceros que solo sabe mutar objetos planos, y funcionará. Esa naturalidad no es un espejismo: es el argumento entero a favor del primitivo.
- Escribir estado con sintaxis imperativa plena y confirmar que sigue siendo reactivo.
- Definir getters que se comportan como derivaciones y setters que interceptan escrituras.
- Entender cómo el proxy interopera con APIs y librerías que mutan objetos.
- Reconocer qué patrones —OOP, formularios, motores— desbloquea esta ergonomía.
Imperativo de verdad, no imitado
En un signal o un store, la mutación en sitio es un pecado: arr.push(x) no notifica porque conserva la referencia. En un createMutable, ese pecado se convierte en la vía correcta. El proxy intercepta las operaciones de array, el incremento, el borrado de claves, y traduce cada una en la notificación granular adecuada.
import { createMutable } from "solid-js/store";
const state = createMutable({
items: [] as string[],
contador: 0,
config: { tema: "oscuro" } as { tema: string; zoom?: number },
});
state.items.push("a"); // reactivo: notifica a quien lea items
state.contador++; // reactivo: lee y reasigna en una expresion
state.config.zoom = 1.5; // reactivo: anade clave nueva
delete state.config.tema; // reactivo: borra clave existente
Nada de esto requiere pensar en referencias, spread ni actualizadores funcionales. El código dice lo que hace y hace lo que dice. Para lógica intrínsecamente imperativa —una máquina de estados de juego, un acumulador que crece en un bucle, una estructura que se edita paso a paso— esta transparencia elimina toda la fricción entre la intención y la sintaxis. Escribes el algoritmo, no su codificación inmutable.
Si te encuentras convirtiendo n++ en setN(v => v + 1) o arr.push(x) en setArr(a => [...a, x]) decenas de veces en un módulo densamente imperativo, ese módulo es exactamente donde createMutable reduce más ruido. La regla no es “usa mutable siempre”, sino “reconoce el código cuya forma natural es la mutación y no lo disfraces de inmutable a la fuerza”.
Getters como derivaciones, setters como intercepción
Como todo store, createMutable respeta los descriptores de propiedad del objeto inicial. Un getter deja de ser un campo y se vuelve una derivación reactiva: se recalcula cuando cambian las propiedades que lee, y sus lectores se suscriben a él como a cualquier fuente. Un setter te da un punto de intercepción para validar, normalizar o descomponer una asignación.
const usuario = createMutable({
nombre: "Grace",
apellido: "Hopper",
get completo() {
return `${this.nombre} ${this.apellido}`;
},
set completo(valor: string) {
const [n, a] = valor.split(" ");
this.nombre = n;
this.apellido = a;
},
});
usuario.completo; // "Grace Hopper" (derivado)
usuario.completo = "Ada Lovelace"; // el setter descompone: nombre y apellido cambian
usuario.completo; // "Ada Lovelace"
El this dentro del getter y del setter apunta al propio proxy, de modo que leer this.nombre teje la dependencia y asignar this.nombre propaga el cambio. Esto acerca createMutable al modelo mental de las clases con propiedades computadas: encapsulas la lógica derivada junto a los datos, en el mismo objeto, en lugar de repartirla entre signals y memos externos. Para quien viene de un mundo orientado a objetos, es el estado reactivo que menos traducción mental exige.
flowchart TD W[usuario.completo = Ada Lovelace] --> S[setter intercepta] S --> N1[this.nombre = Ada] S --> N2[this.apellido = Lovelace] N1 --> R[lectores de nombre y del getter recalculan] N2 --> R style W fill:#89b4fa,color:#11111b style S fill:#f9e2af,color:#11111b style R fill:#a6e3a1,color:#11111b
Dos matices para no idealizar los getters. Primero, no se memoizan solos: cada lectura vuelve a ejecutar el cuerpo del getter, así que una derivación cara —un filtrado, un ordenamiento— conviene respaldarla con un createMemo externo en lugar de dejarla recomputar en cada acceso. Segundo, el this solo apunta al proxy si el getter, el setter o el método son funciones normales; si los defines como funciones flecha, this no se liga al objeto y la reactividad no se teje. La sintaxis abreviada de método —play() { ... }— y el get/set clásicos son la forma segura.
Al modelar comportamiento sobre el proxy, escribe los métodos con la sintaxis abreviada —avanzar(dt) { this.tiempo += dt }— y nunca como avanzar: (dt) => { ... }. La flecha captura el this del ámbito donde se definió, que casi nunca es el proxy, y entonces this.tiempo deja de leer y escribir el estado reactivo. Es un fallo silencioso: compila, parece correcto, pero no propaga.
Interoperar con código que solo sabe mutar
El tercer punto fuerte es el menos glamuroso y el más decisivo en la práctica: hay APIs y librerías enteras cuyo contrato es escribir sobre un objeto que tú les das. Un motor de física que actualiza cuerpo.x cada frame, una librería de formularios que asigna form.campo = valor, un binding de terceros que espera un objeto plano y lo muta sin preguntar. Con un store, ese código rompería la reactividad, porque mutar el proxy por fuera de setStore no se rastrea. Con createMutable, la mutación externa es la vía reactiva: la librería escribe como siempre y Solid se entera igual.
const fisica = createMutable({ x: 0, y: 0, vx: 2, vy: 1 });
// Un bucle externo que ni conoce Solid muta el objeto en cada frame
function paso(cuerpo: { x: number; y: number; vx: number; vy: number }) {
cuerpo.x += cuerpo.vx;
cuerpo.y += cuerpo.vy;
}
// La vista que lee fisica.x y fisica.y se actualiza sola
requestAnimationFrame(function bucle() {
paso(fisica); // mutacion externa, agnostica de Solid
requestAnimationFrame(bucle);
});
La clave conceptual es que el proxy es transparente al patrón de escritura: no exige que quien muta conozca Solid, ni que pase por una API especial. Cualquier código que trate al objeto como mutable —tuyo o de un tercero— queda automáticamente conectado al grafo. Esa es la razón por la que createMutable sobrevive como herramienta de interoperabilidad aunque se desaconseje para el estado de aplicación ordinario: es el único puente que no pide permiso a la librería del otro lado.
Getters como derivaciones
Un get en el objeto inicial se comporta como una derivación: se recalcula al cambiar sus fuentes y sus lectores se suscriben. Encapsula la lógica derivada junto a los datos.
Setters como guardas
Un set intercepta la asignación para validar, normalizar o descomponer. this apunta al proxy, así que dentro puedes leer y escribir otras propiedades y todo propaga.
Interop sin fricción
Motores, formularios y bindings que mutan objetos planos quedan conectados al grafo sin adaptador: su escritura natural es, en el proxy, la vía reactiva.
Todo lo que hace único a este primitivo se reduce a una idea: elimina la capa de traducción entre el modelo mental imperativo y la maquinaria reactiva. En el resto de Solid, y en casi toda la reactividad moderna, existe una brecha que el programador cruza a mano constantemente —piensas “incrementa el contador” pero escribes “llama al setter con una función que devuelve el valor más uno”; piensas “añade este elemento” pero escribes “reemplaza el array por una copia con el elemento al final”—. Esa traducción es barata una vez y cara mil veces, y sobre todo es una fuente silenciosa de errores, porque cada vez que la haces a mano puedes equivocarte de forma que compila pero no reacciona. createMutable borra la brecha: el pensamiento imperativo y el código imperativo vuelven a coincidir, y la reactividad se ocupa por debajo. Getters y setters extienden esa coincidencia al terreno de las clases, dejándote encapsular derivaciones y validaciones donde un desarrollador orientado a objetos las buscaría. Y la interoperabilidad la extiende al mundo exterior, permitiendo que código que nunca oyó hablar de Solid participe del grafo solo por mutar un objeto. Pero fíjate en la simetría exacta con la lección anterior: esta ausencia de traducción es la misma moneda que allí llamábamos pérdida de trazabilidad, vista desde la otra cara. Lo que hace el código tan natural de escribir —que cualquier mutación en cualquier sitio simplemente funcione— es lo mismo que lo hace difícil de rastrear cuando algo va mal. La potencia y el peligro de createMutable no son dos propiedades separadas que puedas sopesar por turnos: son una sola propiedad mirada desde dos ángulos. Por eso dominarlo no consiste en aprovechar la potencia y esquivar el peligro, sino en saber reconocer los pocos contextos donde la balanza de esa única propiedad se inclina a tu favor.
- Modela una lista de tareas con
createMutabley edítala solo con métodos mutadores —push,splice, asignación por índice— confirmando que la vista reacciona a cada uno. - Añade un getter
pendientesque cuente las tareas no hechas y verifica que se recalcula solo al cambiar el estado. - Define un setter que al asignar
todasHechas = truemarque cada tarea; comprueba que una sola asignación propaga a toda la lista. - Simula una librería externa con una función que reciba el objeto y lo mute en un bucle; confirma que la reactividad llega sin adaptador.
- Reescribe uno de esos casos con
createStoreysetStore, y compara cuánta traducción imperativo-a-inmutable tuviste que hacer a mano.