Saltar al contenido

Los peligros de createMutable: reactividad implícita y mutación oculta

El reverso de la comodidad: cuando cualquier state.x = 1 en cualquier archivo es un disparador reactivo, la escritura deja de ser localizable y el flujo de datos pierde su forma. Mutaciones a distancia por referencias compartidas, efectos que se disparan sin causa visible, depuración sin censo de escrituras, y la razón de fondo por la que el equipo de Solid desaconseja el primitivo por defecto.

La escritura sin nombre

En un store, encontrar todos los lugares donde el estado cambia es una búsqueda de texto: setStore. Ese nombre es un ancla —cada escritura pasa por él, así que enumerarlas es trivial y auditarlas, posible. En un createMutable, la escritura es una asignación ordinaria, indistinguible de mutar cualquier objeto local. No hay término que buscar, porque state.x = 1, usuario.nombre = n y carrito.total += p no comparten ninguna marca sintáctica que los delate como reactivos.

TSX
// createStore: el censo de escrituras es buscablesetStore("usuario", "nombre", "Ada");   // grep setStore -> todas las mutaciones// createMutable: la escritura se camufla entre asignaciones cualesquierastate.usuario.nombre = "Ada";           // indistinguible de mutar un objeto local

La consecuencia es que el grafo de “quién cambia qué” deja de vivir en el código y pasa a vivir en tu memoria. Mientras el estado es pequeño y de un solo autor, esa memoria basta. Cuando crece, o cuando el autor eres tú-de-hace-seis-meses, la ausencia de ancla se cobra su precio: cada depuración empieza por reconstruir a mano el mapa de escrituras que el store te habría dado gratis.

Mutación a distancia por referencia compartida

El peligro se agrava porque el proxy es un objeto, y los objetos se pasan por referencia. En cuanto entregas el mutable —o una rama anidada suya— a una función, un componente hijo o una librería, le concedes la capacidad de escribir en tu estado desde un lugar arbitrario del programa. No es un permiso que hayas declarado: es una consecuencia automática de compartir la referencia.

TSX
const state = createMutable({ carrito: { total: 0, items: [] as any[] } });function registrar(destino: { total: number }) {  // esta funcion ni sabe que 'destino' es reactivo  destino.total = 999; // muta el estado global desde aqui}registrar(state.carrito); // accion a distancia: el total cambia, y la vista reacciona

Esto es acción a distancia: un efecto en la interfaz se dispara por una línea escrita en un módulo que no tiene relación aparente con la vista. El vínculo entre causa y efecto existe —la referencia compartida— pero es invisible en el punto de lectura. Depurar se convierte en rastrear quién más tiene un alias del objeto, una pregunta que el lenguaje no responde y que el store, al canalizar toda escritura por su setter, hacía innecesaria.

Sin un censo de escrituras, además, tres actividades cotidianas se encarecen. Depurar, porque no puedes partir del conjunto de mutaciones para acotar la causa de un cambio inesperado. Refactorizar, porque mover o extraer código que muta el proxy puede alterar el orden o el momento de escrituras que nadie había catalogado como tales. Y razonar en equipo, porque el contrato “así cambia este estado” no está escrito en ninguna parte que puedas señalar en una revisión.

TSX
// Un bug clasico del mutable: aliasing accidentalconst config = createMutable({ tema: { color: "azul" } });const tema = config.tema;        // alias de la rama anidadatema.color = "rojo";             // muta el estado global sin mencionar 'config'// meses despues, alguien lee 'tema' creyendo que es una copia local... no lo es

Ninguno de estos costes aparece en el prototipo. Todos aparecen en el mantenimiento, que es donde vive la mayor parte de la vida de un programa. Es la firma típica de una herramienta que optimiza la escritura inicial a expensas de la comprensión posterior: rápida de teclear, lenta de sostener.

El servidor: mutación que se filtra entre peticiones

Hay un escenario donde el peligro deja de ser de mantenimiento y se vuelve un fallo de corrección grave: el renderizado en servidor. Un createMutable definido a nivel de módulo se crea una sola vez por proceso y se comparte entre todas las peticiones que ese proceso atiende. Si una petición muta el proxy durante su render, esa mutación persiste y contamina la siguiente petición —de otro usuario—. Es una fuga de estado entre sesiones, la clase de bug que expone datos de un usuario a otro.

TSX
// PELIGRO en SSR: modulo compartido entre peticionesexport const sesion = createMutable({ usuario: null as string | null });// Si un componente muta esto durante el render en servidor,// el siguiente request hereda el usuario del anteriorsesion.usuario = "ada"; // se filtra fuera de esta peticion

El estado a nivel de módulo es problemático en SSR con cualquier primitivo, pero createMutable lo agrava porque la mutación es implícita: no hay un setStore que delate dónde ocurrió la escritura que filtró el dato. La regla es crear el estado dentro del árbol de componentes o de un contexto por petición, nunca como singleton de módulo mutable. Este matiz, invisible en desarrollo con una sola pestaña, es de los que solo aparecen en producción bajo concurrencia.

Por qué el equipo lo desaconseja por defecto

La documentación de Solid presenta createMutable con una advertencia explícita: es fácil de usar mal y puede llevar a código difícil de razonar, así que no es la vía recomendada para el estado ordinario. El argumento no es que su motor sea inferior —es el mismo de createStore— sino que su contrato con el programador es más débil. La reactividad de grano fino se construye sobre la idea de que las dependencias y los cambios sean explícitos y localizables; createMutable sacrifica esa localizabilidad de la escritura a cambio de ergonomía, y ese sacrificio va en contra del grano de la biblioteca.