Cuándo (casi nunca) usar createMutable: interop OOP y migración desde MobX
El mapa de los pocos contextos donde createMutable es la elección defendible: interop con librerías orientadas a objetos que mutan, migración incremental desde MobX o Vue sin reescribir todo de golpe, y estado local encapsulado que nunca fuga el proxy. La disciplina que lo vuelve seguro —encapsular y no filtrar— y las alternativas más seguras para todo lo demás: createStore, o createStore con produce.
“Casi nunca” no es “nunca”. createMutable sobrevive en la API de Solid porque hay contextos concretos donde su contrato débil deja de ser un defecto y pasa a ser exactamente la pieza que necesitas: cuando el otro lado de una frontera —una librería, un código heredado, un motor externo— habla el idioma de la mutación y traducirlo saldría más caro que aislarlo. Esta lección dibuja ese mapa con precisión: los tres lugares donde el primitivo es defendible, la disciplina que lo mantiene seguro dentro de ellos, y las alternativas a las que volver en cuanto sales.
- Identificar los tres contextos legítimos: interop OOP, migración incremental y estado local encapsulado.
- Aplicar la disciplina que contiene el riesgo: encapsular y nunca filtrar el proxy crudo.
- Trazar una migración desde MobX mapeando observables, computados y acciones.
- Elegir la alternativa más segura —
createStore, ocreateStoreconproduce— por defecto.
Los tres contextos defendibles
El primero es la interoperabilidad con código que muta: un motor de física o de juego, una librería de canvas, un binding de formularios de terceros, cualquier API cuyo contrato sea escribir sobre un objeto que le entregas. Aquí el mutable no es una comodidad, es el único puente que no obliga a reescribir la librería. El segundo es la migración incremental desde un sistema de observables mutables —MobX, Vue— donde reescribir todo el estado al modelo canalizado de golpe sería inviable; el mutable te deja portar módulo a módulo. El tercero es el estado local y encapsulado: una lógica imperativa autocontenida —una máquina de estados, un acumulador complejo— cuyo proxy nunca abandona su módulo.
import { createMutable } from "solid-js/store";
// Contexto 3: encapsulado. El proxy vive y muere dentro de esta funcion.
function crearReproductor() {
const s = createMutable({ tiempo: 0, reproduciendo: false });
// Se expone una superficie controlada, NO el proxy crudo
return {
get tiempo() { return s.tiempo; },
get reproduciendo() { return s.reproduciendo; },
play() { s.reproduciendo = true; },
pause() { s.reproduciendo = false; },
avanzar(dt: number) { s.tiempo += dt; },
};
}
Fíjate en lo que hace seguro al tercer caso: el proxy s nunca sale. Lo que se devuelve es una fachada de getters de lectura y métodos de escritura con nombre. Has reconstruido, a mano y en pequeño, el contrato explícito que createMutable disuelve —y esa es precisamente la disciplina que lo vuelve tolerable.
El daño de createMutable entra por la referencia compartida. Si el proxy nunca cruza la frontera de su módulo —si al exterior solo le das getters y métodos de acción— la mutación a distancia se vuelve imposible por construcción, porque nadie de fuera tiene el objeto que mutar. Encapsular no reduce el riesgo, lo elimina en su origen.
Migrar desde MobX sin reescribir todo
MobX y createMutable comparten modelo mental —estado mutable observable, derivaciones automáticas— lo que hace la traducción casi mecánica. Un makeAutoObservable se vuelve un createMutable; los computed se vuelven getters; las acciones se vuelven métodos que mutan this. La afinidad es tan alta que puedes migrar clase por clase manteniendo el resto de la app funcionando.
// Antes (MobX)
// class Tienda {
// items = [];
// get total() { return this.items.reduce((s, i) => s + i.precio, 0); }
// anadir(i) { this.items.push(i); }
// }
// makeAutoObservable(new Tienda());
// Despues (Solid, createMutable): traduccion casi 1 a 1
const tienda = createMutable({
items: [] as { precio: number }[],
get total() { return this.items.reduce((s, i) => s + i.precio, 0); },
anadir(i: { precio: number }) { this.items.push(i); },
});
El valor de createMutable en la migración es táctico, no estratégico: es un puente para no bloquear el proyecto, no el destino. Una vez portada la app, el consejo del equipo sigue vigente —evaluar si ese estado gana claridad convertido a createStore—. Pero un puente que te deja cruzar sin demoler la orilla vale su peso, y aquí la afinidad de modelos lo convierte en el camino de menor resistencia.
flowchart TD
Q[necesito estado reactivo] --> A{el otro lado muta el objeto}
A -->|si motor libreria binding| M1[createMutable como puente de interop]
A -->|no| B{migro desde MobX o Vue}
B -->|si, gran base heredada| M2[createMutable modulo a modulo]
B -->|no| C{el proxy queda encerrado en un modulo}
C -->|si, fachada de getters y metodos| M3[createMutable local aceptable]
C -->|no, se comparte o crece| D[usa createStore por defecto]
style D fill:#a6e3a1,color:#11111b
style M1 fill:#f9e2af,color:#11111b
style M2 fill:#f9e2af,color:#11111b
style M3 fill:#f9e2af,color:#11111bLas alternativas más seguras
Para casi todo lo demás, la respuesta es createStore. Te da la misma reactividad de grano fino y el mismo envoltorio profundo, pero canaliza la escritura por setStore, devolviéndote el censo de mutaciones y el flujo explícito. Y si lo que te atraía del mutable era escribir como si mutaras, no necesitas renunciar a eso: setStore acepta produce, que te da un borrador mutable dentro de una función controlada, con la escritura imperativa por dentro y el contrato explícito por fuera.
import { createStore, produce } from "solid-js/store";
const [store, setStore] = createStore({ items: [] as string[], contador: 0 });
// Ergonomia imperativa DENTRO, escritura canalizada FUERA
setStore(produce((s) => {
s.items.push("a"); // se lee y escribe como un mutable
s.contador++; // pero todo ocurre dentro de setStore
}));
Este patrón —que la lección 5 desarrolla a fondo— es la síntesis que casi siempre buscabas: la comodidad de createMutable en el punto de escritura, sin perder la localizabilidad que da tener un setStore como ancla. Antes de alcanzar el mutable, pregúntate si produce no te da ya el 90% de lo que querías sin ninguno de los peligros.
Defendible
Interop con librerías que mutan. Migración incremental desde MobX o Vue. Estado local cuyo proxy nunca sale del módulo tras una fachada.
Injustificado
Estado de aplicación compartido, estado de formulario global, cualquier proxy que viaje entre componentes. Ahí createStore es superior sin contrapartida.
Si lo que buscabas era encapsular estado y comportamiento al estilo OOP, no siempre necesitas el mutable. Una clase corriente cuyos campos son signals —o que expone getters respaldados por memos— te da encapsulación, métodos de acción con nombre y reactividad de grano fino, con la escritura canalizada por los setters de cada signal. Es más verbosa que createMutable, pero conserva el contrato explícito, y para lógica de dominio compleja suele envejecer mejor.
Casi todas las discusiones sobre este primitivo se plantean mal, como un juicio moral —es bueno o es malo, se usa o no se usa—. La forma útil de pensarlo es geográfica: createMutable es una herramienta de frontera, y su valor entero depende de dónde dibujes esa frontera. En el borde exacto donde tu código reactivo se encuentra con algo que habla el idioma de la mutación —una librería ajena, una base heredada de MobX, un motor que escribe cada frame— el mutable es el traductor natural, porque hablar mutación es precisamente lo que se le pide. El error nunca es usarlo en esa frontera; el error es dejar que la frontera se expanda. Un mutable que empieza aislando la interop con un motor de física y termina siendo el estado global de media aplicación no ha fallado por lo que es, sino por haber crecido más allá del borde donde tenía sentido. Por eso la disciplina que importa no es una lista de prohibiciones sino una sola vigilancia: mantener el proxy encerrado tras una fachada de getters de lectura y métodos de acción, de modo que la mutación libre viva confinada en el módulo que la necesita y el resto del programa solo vea el contrato explícito de siempre. Hecho así, createMutable deja de ser un riesgo para volverse lo que debe ser —una junta de estanqueidad entre dos mundos con contratos distintos, invisible desde ambos lados—. Y en cuanto notes que esa junta empieza a filtrar, que el proxy viaja donde no debía o que la comodidad te tienta a extenderlo, la respuesta ya la conoces: createStore con produce te espera con casi toda la ergonomía y ninguno de los peligros. Saber trazar y defender esa frontera es la competencia real; todo lo demás es sintaxis.
- Escribe un módulo que encapsule un
createMutabletras una fachada de getters y métodos, y verifica desde fuera que no puedes obtener el proxy crudo. - Toma una clase de MobX de ejemplo y tradúcela a
createMutablemapeando observables a campos, computados a getters y acciones a métodos. - Reescribe ese mismo estado con
createStoreyproduce; compara la ergonomía de escritura línea a línea. - Enumera tres lugares de una app real donde usar
createMutablesería un error y justifica con qué alternativa lo sustituirías. - Redacta tu propia regla de una frase para decidir, en caliente, entre
createMutableycreateStore.