Signals en el lenguaje
Durante quince años cada framework reimplementó el mismo primitivo de reactividad con nombres distintos y semánticas incompatibles; el TC39 Signals proposal termina esa duplicación llevando el grafo de dependencias al propio lenguaje. Esta lección diseca qué estandariza exactamente la propuesta —un estado escribible, una derivación perezosa y un observador de bajo nivel deliberadamente escondido— y, sobre todo, por qué se detiene justo antes del efecto, dejando la planificación en manos de cada framework. De ahí salen las consecuencias reales de que la reactividad sea nativa del navegador: librerías de estado que funcionan en cualquier framework, herramientas que ven el grafo sin adaptadores, componentes web con reactividad sin runtime propio, y una frontera nueva entre lo que el estándar garantiza y lo que seguirá siendo decisión de arquitectura tuya.
Hay un momento en la vida de toda idea de infraestructura en que deja de ser una librería y pasa a ser una capa. Los promises lo vivieron: primero fueron cinco implementaciones rivales, después una especificación, después sintaxis. La reactividad de grano fino está exactamente en ese punto. Cada framework de la última década reimplementó el mismo grafo de dependencias con otro nombre —ref, signal, observable, atom, state— y con semánticas parecidas pero incompatibles en los bordes. El TC39 Signals proposal propone dejar de repetir esa tarea y meter el primitivo en el lenguaje, con una precisión quirúrgica sobre dónde termina el estándar. Esta lección no es sobre la sintaxis de una propuesta: es sobre qué cambia cuando el grafo deja de ser tuyo y pasa a ser del navegador.
- Delimitar con exactitud qué parte de la reactividad estandariza la propuesta y cuál deja fuera a propósito.
- Explicar por qué el observador vive tras un espacio de nombres marcado como delicado y qué frontera dibuja.
- Anticipar qué se desbloquea de verdad cuando varios frameworks comparten un único grafo de dependencias.
- Separar lo que un estándar resuelve de lo que seguirá siendo una decisión de arquitectura del programador.
El núcleo mínimo: un estado, una derivación y un vigilante
La propuesta es sorprendentemente pequeña, y esa pequeñez es su tesis. Hay un contenedor de valor escribible, una derivación perezosa que se recalcula sola cuando alguna de sus fuentes cambia, y un observador de bajo nivel que avisa de que algo en su ámbito quedó sucio.
const contador = new Signal.State(0)
const doble = new Signal.Computed(() => contador.get() * 2)
contador.get() // 0
doble.get() // 0
contador.set(5)
doble.get() // 10, recalculado en la lectura, no en la escritura
Todo lo interesante está en el comportamiento, no en la forma. La derivación es perezosa: escribir no dispara ningún cálculo, solo invalida. El grafo se descubre en la lectura, registrando qué se leyó mientras se evaluaba el cuerpo, de modo que las dependencias condicionales se ajustan solas en cada evaluación. Y la propagación es libre de glitches: nunca observarás una derivación construida a partir de una mezcla de valores viejos y nuevos, porque el recálculo respeta el orden topológico del grafo.
Hay un detalle de la especificación que parece menor y decide el rendimiento de aplicaciones enteras: la comprobación de igualdad. Si una derivación se recalcula y produce un valor idéntico al anterior, la invalidación se detiene ahí y no sigue bajando. Eso convierte al grafo en algo más que un mecanismo de notificación: es un cortafuegos que absorbe los cambios que no cambian nada, y es la razón por la que un grafo bien construido hace mucho menos trabajo del que su tamaño sugiere.
Otro rasgo que conviene registrar es que la propuesta no impone dónde vive el estado. Una señal es un objeto normal: puede estar en un módulo, dentro de una clase, en una estructura de datos o capturada en un cierre. No hay contenedor, no hay proveedor y no hay árbol de componentes que la aloje, y por eso el mismo primitivo sirve igual para el estado global de una aplicación que para un contador dentro de un elemento personalizado.
Hay una tercera pieza, la que hace utilizable a las otras dos desde fuera. Un vigilante recibe una notificación cuando alguna derivación de las que observa deja de ser válida, y a partir de ahí es cosa de quien lo usa decidir qué hacer con esa noticia.
const w = new Signal.subtle.Watcher(() => queueMicrotask(procesar))
w.watch(doble)
function procesar() {
for (const s of w.getPending()) s.get() // releer reengancha el vigilante
w.watch()
}
Ese bucle de tres líneas es, literalmente, el punto donde termina el estándar y empieza tu framework.
Merece la pena mirarlo despacio porque contiene una asimetría deliberada. El vigilante avisa una sola vez y se desengancha; volver a engancharlo es responsabilidad de quien lo usa, y ese diseño evita la avalancha de notificaciones que sufriría un observador que siguiera escuchando mientras se procesa el lote. Es la misma idea de una interrupción que se enmascara mientras se atiende.
Conviene nombrar bien el modelo porque decide cuándo corre tu código. La escritura no empuja valores: marca descendientes como potencialmente sucios y, si hay vigilantes, les avisa una sola vez. El valor real se calcula cuando alguien lo pide. La consecuencia práctica es que una derivación que nadie lee no cuesta nada por mucho que sus fuentes cambien, y que el trabajo se concentra en el instante en que la interfaz de verdad lo necesita. Es la misma disciplina de perezosa con invalidación que has visto en la memoización y en el estado derivado, ahora con garantías escritas en una especificación en lugar de en el README de una librería.
Por qué el estándar se detiene justo antes del efecto
La ausencia más llamativa de la propuesta es la que más se ha discutido: no hay una función de efecto. No existe nada equivalente a lo que en cada framework se llama effect, watchEffect o createEffect. Y no es un olvido, es la decisión central del diseño.
Un efecto no es solo una función que corre cuando algo cambia; es una función que corre cuando alguien decide que debe correr. Y ese cuándo es precisamente lo que ningún framework quiere ceder. Uno agrupa en un microtask, otro espera al siguiente fotograma de animación, otro ejecuta de forma síncrona, otro lo mete en su propia cola de prioridades para poder interrumpirla. La planificación es identidad de framework, no mecánica de grafo.
Por eso el vigilante se expone bajo un espacio de nombres explícitamente marcado como delicado. El nombre es una señal social tanto como técnica: dice que esa API no está pensada para el código de aplicación, sino para quien construye la capa de arriba, y que usarla directamente significa asumir la responsabilidad de reenganchar, de no crear fugas y de no llamar a nada reactivo desde dentro de la notificación.
Ese cuidado no es paranoia de comité. La notificación de un vigilante ocurre en un momento en el que el grafo está a medio invalidar, y leer o escribir señales desde ahí es la forma más rápida de producir un ciclo o un valor a medio cocer. La regla que la especificación pide respetar es sencilla de enunciar y fácil de romper: dentro de la notificación solo se planifica trabajo, nunca se hace.
Hay una segunda razón, menos comentada, para dejar el efecto fuera. Un efecto necesita un dueño que lo libere, y la propiedad de recursos es un problema entero de por sí: cuándo se destruye, quién lo destruye, qué pasa con los efectos anidados y qué ocurre si el componente que lo creó desaparece a mitad de una actualización. Cada framework tiene ya un modelo de propiedad, y meter otro en el lenguaje habría obligado a todos a reconciliar el suyo con uno ajeno.
flowchart TD A[Signal State escribible] --> B[Signal Computed perezoso] B --> C[Watcher en subtle] C --> D[planificador del framework] D --> E[microtask] D --> F[fotograma de animacion] D --> G[cola con prioridades] E --> H[actualizacion del DOM] F --> H G --> H style A fill:#89b4fa,color:#11111b style C fill:#f9e2af,color:#11111b style D fill:#cba6f7,color:#11111b
La lectura arquitectónica de ese corte es elegante. El estándar se queda con la parte que es matemática —quién depende de quién, en qué orden recalcular, cómo evitar valores incoherentes— y devuelve al ecosistema la parte que es política —cuándo interrumpir al usuario para repintar—. La primera tiene una única respuesta correcta y merece estar en el motor; la segunda tiene tantas respuestas legítimas como productos, y congelarla en una especificación habría sido un error irreversible.
Lo que cambia cuando el grafo pertenece al navegador
Estado que cruza fronteras
Una librería de estado escrita sobre el primitivo estándar funciona sin adaptadores en cualquier framework que lo consuma. El store deja de tener nacionalidad.
Componentes web con reactividad
Un elemento personalizado puede ser reactivo sin embarcar un runtime propio, que es la razón por la que los componentes web nunca terminaron de despegar.
Herramientas que ven el grafo
Si el motor conoce las dependencias, depurar deja de depender de una extensión por framework: el grafo es inspeccionable como hoy lo es la pila de llamadas.
Optimización bajo la línea
Un primitivo en el motor puede optimizarse en el nivel del propio navegador, con estructuras que ninguna librería en JavaScript puede permitirse.
De las cuatro, la que más consecuencias tendrá a medio plazo es la tercera. Hoy, depurar por qué algo no se actualiza depende de que exista una extensión mantenida para tu framework, en tu versión, con tu configuración de compilación. Si el grafo pertenece al motor, ese diagnóstico se vuelve una capacidad del navegador, igual que hoy lo es inspeccionar una pila de llamadas o un temporizador pendiente.
La consecuencia más profunda no es ninguna de las cuatro por separado, sino lo que tienen en común: la reactividad deja de ser una propiedad del framework y pasa a ser una propiedad del dato. Hoy, migrar de framework significa reescribir el estado porque el estado está tejido con las primitivas de ese framework. Cuando la primitiva es del lenguaje, el estado sobrevive a la migración y lo que se reescribe es solo la capa de vista, que es lo que siempre debió ser desechable.
Hay además un efecto de segundo orden en las fronteras raras. Una micro aplicación escrita en un framework y otra escrita en otro pueden compartir una porción de estado sin puentes ni serialización. Una isla hidratada puede leer el mismo grafo que el código que la rodea. Un widget de terceros incrustado en tu página puede reaccionar a tu tema sin exponerte a su runtime.
El caso de los componentes web merece detenerse porque explica un fracaso de quince años. La plataforma les dio encapsulación, ciclo de vida y estilos aislados, pero nunca les dio una forma de decir que cuando este dato cambie, ese texto se actualice. Sin eso, cada librería de componentes web tuvo que embarcar su propio motor de reactividad, y el resultado fue el peor de los mundos: estándares por fuera y fragmentación por dentro. Un primitivo común cierra ese hueco.
Y hay un beneficio que solo se aprecia con el tiempo: la portabilidad del conocimiento. Cuando el grafo es el mismo para todos, lo que aprendes sobre pereza, invalidación, igualdad y orden topológico se transfiere entero de un framework a otro. Deja de haber cinco reactividades que aprender por separado y pasa a haber una, con cinco capas de planificación encima.
No hace falta esperar a que la propuesta llegue para cobrar parte del beneficio. Basta con una disciplina de diseño: mantén la lógica de estado en módulos que no importen nada de tu framework, expón su superficie como lecturas y escrituras sin hooks, y deja que la capa de vista sea un adaptador delgado que se suscribe. Ese corte es exactamente la frontera que el estándar va a formalizar, y trazarlo ahora tiene dos premios inmediatos que no dependen de ningún calendario: puedes testear el estado sin montar la interfaz, y el día que quieras cambiar de framework solo reescribes el adaptador.
Lo que estandarizar no arregla
Conviene bajar las expectativas donde toca. Estandarizar la propagación no estandariza la disciplina de mutación: el primitivo sigue permitiendo que cualquiera escriba en cualquier momento, y toda la ingeniería de contratos que has aprendido —reducers, transiciones declaradas, efectos como valores— sigue siendo tan necesaria como antes y sigue siendo tuya.
Tampoco resuelve la asincronía. El grafo estándar es síncrono: una derivación devuelve un valor, no una promesa. El estado de carga, la cancelación de peticiones obsoletas y la reconciliación con el servidor quedan fuera, y es justamente la frontera que explora la lección siguiente.
Ni resuelve la granularidad de tu aplicación, que es distinta de la granularidad del mecanismo. Puedes tener el grafo más fino del mundo y meter todo el estado en un único objeto que se reemplaza entero en cada cambio; el motor propagará con precisión quirúrgica una invalidación que abarca la aplicación completa. La forma en que partes tu estado sigue siendo tuya, y sigue determinando cuánto trabajo cuesta cada actualización.
Y hay un coste que se paga por adelantado: lo que entra en una especificación deja de poder iterar rápido. Una librería corrige su semántica de propiedad y limpieza en una versión menor; el lenguaje no. Por eso la propuesta prefiere quedarse corta y dejar espacio, aceptando que algunas preguntas —quién es dueño de un signal, cuándo se libera, cómo se transporta entre hilos— se respondan primero en el ecosistema durante unos años.
Tampoco elimina la fragmentación, la traslada. Dos frameworks que compartan el grafo seguirán planificando distinto, y un componente que dependa de que su efecto corra de forma síncrona no se comportará igual dentro de un anfitrión que agrupa en microtask. La interoperabilidad que se gana es la del dato; la del comportamiento temporal sigue siendo un acuerdo entre partes.
Queda por último una advertencia de calendario. Una propuesta de este tipo tarda años en atravesar sus etapas, y durante ese tiempo convive con implementaciones parciales, adaptaciones que emulan la API y frameworks que la adoptan por dentro sin exponerla. La transición no será un día concreto en que todo cambie, sino un periodo largo en el que aprender el modelo importa más que aprender la sintaxis.
El error de lectura más caro sería creer que, con signals en el lenguaje, las decisiones de arquitectura se vuelven innecesarias. No lo hacen. Seguirás teniendo que separar el estado de servidor del estado de cliente, seguirás teniendo que decidir si esta pantalla merece una máquina o un valor suelto, y seguirás pudiendo construir un desastre perfectamente reactivo. El estándar mejora el mecanismo de propagación, que era el eje en el que menos te equivocabas; el eje de la disciplina, donde nacen los bugs difíciles, sigue exactamente igual de vacío que antes.
Hay una regla histórica que conviene leer aquí porque explica la próxima década mejor que cualquier tabla comparativa: cuando una idea repetida por todos se absorbe en la plataforma, no desaparece la competencia, se desplaza un nivel hacia arriba. Ocurrió con los promises, y las librerías de control de flujo no murieron: se convirtieron en librerías de concurrencia y cancelación, un problema más alto. Ocurrió con el módulo, y los empaquetadores no murieron: dejaron de discutir formatos y empezaron a competir en velocidad y en división de código. Con la reactividad pasará lo mismo. El día que el grafo sea del motor, ningún framework podrá seguir vendiéndote su propagación, porque será la misma propagación para todos. Lo que quedará en juego —y por tanto donde se jugará la diferenciación real— es lo que el estándar dejó fuera a propósito: cómo se planifica el trabajo, cómo se integra la asincronía, cómo se sincroniza con el servidor y qué disciplina de mutación se impone encima. Ese desplazamiento tiene una consecuencia directa para ti. Si has aprendido reactividad como el conjunto de trucos de una librería concreta, tu conocimiento caduca en el momento exacto en que esos trucos se vuelven una clase del lenguaje. Si la has aprendido como grafo de dependencias, pereza, invalidación y orden topológico, el estándar no te obsoleta: te confirma, y encima te regala una base común sobre la que por fin todo el ecosistema puede construir en la misma dirección. Los primitivos suben a la plataforma; el criterio para usarlos nunca sube, se queda contigo.
- Implementa un
effectmínimo sobre el vigilante estándar: crea la derivación, obsérvala, y en la notificación relee lo pendiente y vuelve a engancharlo. - Cambia la planificación de tu efecto de microtask a fotograma de animación y observa qué se ve distinto en pantalla con actualizaciones rápidas seguidas.
- Escribe una derivación con una dependencia condicional y demuestra, contando ejecuciones, que la arista desaparece cuando la rama deja de tomarse.
- Construye un diamante de dependencias y verifica que el nodo final se recalcula una sola vez por escritura, no dos.
- Toma un store que uses a diario y reescribe solo su motor de propagación sobre el primitivo estándar, dejando intacta su superficie pública.
- Argumenta con un caso concreto qué parte de tu código de estado sobreviviría a un cambio de framework si el grafo fuese del lenguaje.