Por qué son mutuamente incompatibles
Cinco motores que resuelven el mismo problema no interoperan porque el grafo es una estructura global de módulo y las garantías de orden no componen. La razón técnica de la fragmentación y qué intenta arreglar la estandarización.
Un computed de Vue no se puede leer desde un createMemo de Solid y esperar que la dependencia se registre. No es por falta de voluntad ni por licencias: es una imposibilidad estructural que se deduce directamente del diseño de las cinco piezas. Entender por qué es imposible explica a la vez la fragmentación del ecosistema y qué es exactamente lo que la propuesta de Signals en TC39 intenta desbloquear.
- Explicar por qué el observador actual impide la interoperabilidad entre motores.
- Ver que las garantías de consistencia no componen entre grafos separados.
- Distinguir interoperabilidad de bajo nivel de compatibilidad de API.
- Situar qué problema concreto ataca la estandarización del primitivo.
El grafo es un singleton de módulo
Vuelve a la pieza 3 del inventario anterior: el observador actual es una variable de módulo. Solid tiene su Listener; Vue tiene el suyo; Angular tiene el suyo. Son tres variables distintas en tres módulos distintos, y ninguna sabe de la existencia de las otras.
Sigue la ejecución. Estás dentro de un createMemo de Solid, así que el Listener de Solid apunta a esa computación. Lees un computed de Vue. El getter de Vue consulta su variable, que vale null, concluye que nadie lo está observando y no teje ninguna arista. Devuelve el valor correcto en ese instante, pero la dependencia no queda registrada. Cuando la fuente de Vue cambie, el memo de Solid no se enterará jamás.
// Lo que ocurre de verdad, esquematizado
// modulo solid.js
let ListenerSolid = null;
// modulo vue.js
let activeSubVue = null;
// dentro de un memo de Solid: ListenerSolid = memo, activeSubVue = null
leerRefDeVue(); // consulta activeSubVue, ve null, no registra nada
El fallo es especialmente cruel porque no es un error visible. No hay excepción, no hay aviso: el valor leído es correcto y el programa parece funcionar. El síntoma es que una parte de la interfaz simplemente deja de actualizarse, a veces solo en ciertos caminos de código. Es exactamente el mismo modo de fallo que produce tener dos copias del mismo motor en el bundle.
Las garantías de orden no componen
Aunque resolvieras el tracking —por ejemplo, con adaptadores que registren en los dos sistemas—, quedaría un problema más profundo: las garantías de consistencia son propiedades globales del grafo, y dos grafos separados no pueden garantizar conjuntamente lo que cada uno garantiza por separado.
La consistencia glitch-free significa que ninguna computación se evalúa antes que sus dependencias transitivas. Para asegurarlo, el motor necesita conocer el orden topológico del grafo entero. Si la mitad de las aristas viven en otro motor, tu orden topológico está incompleto: puedes cumplir el orden entre tus nodos y aun así ejecutar uno antes de que se haya actualizado un nodo ajeno del que depende.
flowchart TB A[fuente en motor A] --> B[derivado en motor A] A --> C[derivado en motor B] B --> D[efecto en motor A] C --> D style A fill:#89b4fa,color:#11111b style B fill:#cba6f7,color:#11111b style C fill:#f38ba8,color:#11111b style D fill:#f9e2af,color:#11111b
En este rombo, el efecto de la derecha depende de dos caminos que atraviesan motores distintos. El motor A puede ordenar su rama, pero no sabe si el motor B ya recalculó la suya. El resultado es un glitch que ninguno de los dos motores puede prevenir, porque cada uno cumple su contrato local perfectamente. La consistencia no es composicional, y esa es la razón técnica de fondo por la que dos motores no pueden compartir un grafo aunque quieran.
Que no puedan compartir grafo no significa que no puedan comunicarse. Un puente típico suscribe un efecto de A que escribe una fuente de B. Eso funciona, y es lo que hacen las integraciones reales entre señales y observables. Lo que cambia es el contrato: la escritura en B ocurre después de que A haya terminado su propagación, así que se introduce un salto adicional en la cascada y se pierde la garantía glitch-free entre los dos lados. Es una decisión perfectamente razonable, pero conviene saber que se está tomando.
Interoperabilidad no es compatibilidad de API
Conviene separar dos cosas que se confunden. La compatibilidad de API es que dos librerías se usen igual: signal() y .value en las dos. Es superficial y no compra nada; un adaptador de treinta líneas la consigue y no arregla ninguno de los dos problemas anteriores.
La interoperabilidad de bajo nivel es que dos librerías compartan el mismo grafo: que una arista tejida desde una se registre en la otra, y que el orden topológico sea único y global. Eso solo se consigue si las dos usan la misma variable de observador actual y el mismo algoritmo de propagación.
Y esa es precisamente la apuesta de la propuesta de Signals en TC39: no estandarizar una sintaxis bonita, sino colocar el primitivo —y con él la variable de observador actual y la propagación glitch-free— en el propio motor de JavaScript, donde solo puede haber una copia. Si el primitivo vive en el lenguaje, un computed de Vue construido sobre él y un memo de Solid construido sobre él comparten grafo por construcción.
La propuesta está en Stage 1 del proceso de TC39, es decir, el comité ha aceptado que el problema merece explorarse y no hay compromiso alguno sobre la forma final de la API. Su plan declarado es prototipar e integrar en varios frameworks antes de intentar avanzar de etapa. Hay un polyfill publicado con el que se puede experimentar hoy. Volveremos sobre ella con detalle en el nivel 12, ya con todo el vocabulario necesario para leer la especificación sin traducciones.
Es cómodo contar la historia de los signals como una duplicación absurda que se podría haber evitado con más coordinación, y es una lectura equivocada. El primitivo llegó a cinco frameworks a la vez porque cinco equipos, con restricciones distintas, convergieron en la misma solución tras probar otras. Knockout tenía observables en 2010; MobX los tenía en 2015; Vue los tuvo desde el principio con otro nombre. Lo que ocurrió entre 2022 y 2024 no fue un descubrimiento sino un reconocimiento colectivo: el modelo ya estaba validado en producción y por fin se aceptó nombrarlo igual. Y la incompatibilidad tampoco es negligencia: es la consecuencia inevitable de que la pieza central del diseño —el observador actual— sea una variable de módulo, algo que ninguno de los cinco podía externalizar sin que existiera un estándar al que externalizarla. La lección que llevarte no es que la industria perdió el tiempo, sino algo más útil para tu propio trabajo: cuando un mecanismo se apoya en un singleton de módulo, has creado una frontera de interoperabilidad, y esa frontera solo se puede mover hacia abajo, hacia una capa que todos compartan. Es exactamente el mismo argumento que llevó a estandarizar las promesas cuando había cinco librerías de thenables incompatibles.
Lo que puedes hacer hoy
Mientras el primitivo no esté en el lenguaje, las reglas prácticas son tres y son aburridas, que es lo que se quiere de una regla práctica.
Un motor por aplicación. Mezclar dos grafos en la misma pantalla es viable pero convierte cada frontera en un punto de sincronización manual que hay que documentar.
Cuando haya que cruzar, cruza por un punto explícito y nombrado: un efecto que escribe en el otro sistema, con la dirección del flujo declarada. Nunca leas una fuente del otro motor dentro de una derivación del tuyo esperando que se registre, porque no lo hará y no te avisará.
Y vigila que solo haya una copia del motor en el bundle. Una duplicación por dependencias transitivas produce exactamente los mismos síntomas que mezclar dos motores distintos, y es mucho más frecuente de lo que parece.
- Instala dos copias distintas de un mismo paquete de señales, forzando versiones que no se deduplican.
- Crea una fuente con una copia y léela dentro de un efecto creado con la otra.
- Escribe la fuente y comprueba que el efecto no se reejecuta y que no hay ningún error.
- Explica, señalando la variable concreta, por qué el fallo es silencioso en vez de ruidoso.