Markdown reactivo: el lenguaje del framework
En el capítulo anterior moviste un control deslizante y viste cómo un gráfico se rehacía sin tocar el servidor. Si en aquel ejercicio reordenaste los bloques js (el deslizador antes que el gráfico, el gráfico antes que el filtro), probablemente notaste algo desconcertante: el resultado seguía siendo el mismo. En un script de Python o de Node.js, alterar el orden de las líneas suele producir un error o un valor incorrecto. Aquí no.
Este capítulo explica por qué. Más que repasar la sintaxis del Markdown extendido del framework, su objetivo es construir un modelo mental: entender que un Markdown reactivo no se ejecuta como un script, sino como una hoja de cálculo textual donde cada bloque es una celda y las dependencias entre celdas determinan el orden de evaluación (Observable, Inc., 2026).
El orden de evaluación en un Markdown reactivo lo decide el grafo de dependencias entre bloques, no la posición del texto. El framework te libera de pensar en secuencia para que pienses en relaciones.
Del script al grafo: tres modelos de ejecución
Para apreciar lo que el framework propone, conviene contrastarlo con dos modelos que probablemente ya conoces. La diferencia no es de sintaxis: es de paradigma.
El script tradicional
Un programa en Python, R o Node.js se lee como una receta: el intérprete recorre las líneas de arriba abajo, ejecuta cada una en su turno y mantiene un estado mutable que cada instrucción puede modificar. Si la línea diez utiliza una variable definida en la cuarenta, hay un error; si redefines una variable a mitad del archivo, el nuevo valor se propaga solo hacia adelante. La secuencia textual es la secuencia de ejecución.
El cuaderno clásico
Los cuadernos al estilo Jupyter relajaron esa rigidez al partir el código en celdas que el usuario corre cuando quiere. La flexibilidad tuvo un precio: el estado del kernel depende del orden histórico en que se ejecutaron las celdas, no del orden en que aparecen en el documento. Es habitual abrir un cuaderno ajeno, ejecutarlo de principio a fin y descubrir que falla, simplemente porque el autor lo armó saltando entre celdas. Este problema, conocido como crisis de reproducibilidad de los cuadernos, ha motivado una nueva generación de entornos (entre ellos Observable) que apuestan por la reactividad como solución (Perkel, 2021).
El Markdown reactivo
Observable Framework adopta una estrategia distinta: cada bloque js declara qué variables produce y cuáles consume. Antes de ejecutar nada, el runtime analiza esas declaraciones, construye un grafo dirigido de dependencias y lo recorre en orden topológico. El resultado es que las celdas independientes pueden evaluarse en paralelo, las celdas dependientes se evalúan después de sus insumos y, cuando una entrada cambia, solo se vuelven a calcular las celdas alcanzables desde ella. La Figura 3.1 ilustra el grafo del ejercicio que armaste en el capítulo 2.
fuentes y umbral son las raíces; el gráfico y el resumen son nodos consumidores. Mover el deslizador no recalcula fuentes (no depende de él); cambiar el CSV no toca el deslizador. El framework recorre el grafo y reevalúa solo lo necesario.Esta intuición es vieja y sólida. La programación literaria de Knuth ya defendía que un programa debía leerse como un ensayo, donde cada fragmento se nombra y se referencia por su sentido y no por su lugar (Knuth, 1984); las explicaciones explorables de Bret Victor llevaron la misma idea al terreno interactivo (Victor, 2011). El Markdown reactivo es, en cierto modo, la materialización en código abierto de ambas tradiciones: prosa que el lector puede inspeccionar y manipular, sin que el autor tenga que coreografiar el orden.
Anatomía de un bloque reactivo
Dentro de un archivo .md, el framework reconoce dos formas de incrustar JavaScript: los bloques cercados con tres acentos graves y la palabra js, y las expresiones en línea entre ${ }. Ambas alimentan el mismo grafo, pero se usan en momentos distintos.
Bloques de expresión y bloques de programa
Un bloque js es una expresión cuando contiene un solo término evaluable (un número, un objeto, una llamada a Plot.plot(...) o un template literal con html `…`) y el framework muestra su resultado de forma automática. Es un programa cuando contiene declaraciones (const, let), varias sentencias o termina con punto y coma: en ese caso no se muestra nada salvo lo que invoques explícitamente con display(...) (Observable, Inc., 2026).
La Tabla 3.1 sintetiza los cuatro patrones que verás a diario.
| Patrón | Ejemplo | ¿Muestra valor? | Cuándo usarlo |
|---|---|---|---|
| Bloque-expresión | ```jsPlot.plot({…})``` |
Sí, automáticamente. | Renderizar un gráfico, una tabla o un fragmento de HTML como salida única del bloque. |
| Bloque-programa | ```jsconst x = 42;``` |
No. | Definir variables que otros bloques consumirán. |
| Expresión en línea | Hoy es ${new Date().toLocaleDateString()}. |
Sí, dentro del párrafo. | Insertar valores breves dentro de la prosa sin romper el flujo de lectura. |
Programa con display |
```jsdisplay(html`<p>${valor}</p>`);``` |
Sí, donde lo invoques. | Mostrar varios resultados desde un mismo bloque o ubicar la salida con precisión. |
La Figura 3.2 ilustra la expresión en línea aplicada a una frase corriente.
Este capítulo se rehace solo cada vez que abres la página: la última recarga ocurrió en el minuto
minutos; la expresión dentro del párrafo lo consume y muestra el valor sin necesidad de un bloque aparte.Ver código
```js
const minutos = Math.round((Date.now() / 1000 / 60) % 60);
```
…en el minuto **${minutos}** de la hora actual.
El framework exige que cada nombre del ámbito superior se declare una sola vez en toda la página. Esa restricción es lo que permite construir el grafo: si dos bloques pudieran redefinir x, no habría manera de saber a cuál apunta una referencia. Para variables auxiliares que no necesitan ser visibles fuera del bloque, envuélvelas en una función o en un bloque { … }; lo que ocurra dentro queda confinado y no compite con el resto del archivo.
El grafo en acción: dependencias y propagación
Hasta aquí la teoría. Veamos cómo se siente en la práctica con una variable compartida entre tres lugares distantes del archivo.
Definimos un deslizador en algún punto del documento:
A partir de ese momento, factor está disponible para cualquier otro bloque o expresión en línea, esté antes o después en el texto. La frase que estás leyendo lo confirma: el factor actual vale
factor y, a través de él, de tabla. Mover el control reevalúa los tres bloques en orden topológico.Ver código
const factor = view(Inputs.range([1, 25], {step: 1, value: 8, label: "Factor"}));
const tabla = Array.from({length: 5}, (_, i) => ({
n: i + 1,
"n × factor": (i + 1) * factor,
"n²": (i + 1) ** 2
}));
display(html`<div class="grid grid-cols-2" style="gap: 1rem;">
<div>${Inputs.table(tabla, {width: 340, rows: 5})}</div>
<div>${Plot.plot({/* … */})}</div>
</div>`);
Observa el flujo: cuando factor cambia, tabla se recalcula porque depende de él; en consecuencia, la tabla y el gráfico se redibujan porque ambos dependen de tabla. El framework decide qué reevaluar siguiendo las aristas del grafo, no las líneas del archivo. Si un bloque no depende de factor ni directa ni indirectamente, simplemente no se toca.
«factor is already defined». Declaraste el mismo nombre en dos bloques. Renombra uno o conviértelo en variable interna de una función.
«let declarations are not allowed». En el ámbito superior solo se admiten const y declaraciones de función. La razón es la misma de la regla anterior: let implica reasignación, y el grafo no podría inferir cuándo propagar el cambio.
Sombrear un nombre del entorno (Plot, d3, html, view) lo deja inutilizable en el resto de la página. Si necesitas una variable con un nombre genérico, prefiérela en plural o usa un sufijo descriptivo (plotConfig, htmlBloque).
Tiempo y estado: promesas, generadores y Mutable
El grafo cubre los valores que cambian cuando el lector mueve un control. Pero, ¿qué hacer con valores que cambian por sí solos: el resultado de una consulta a disco, un reloj que avanza segundo a segundo, un contador que el usuario incrementa con un botón? El framework ofrece tres mecanismos complementarios. Saber cuándo usar cada uno es probablemente lo más útil que puedes llevarte de este capítulo.
Promesas: la asincronía silenciosa
Los bloques js toleran promesas de manera transparente. Cuando un bloque devuelve una Promise (o usa await en su interior), el grafo simplemente espera a que se resuelva antes de propagar el valor. Por eso FileAttachment("...").csv(), que devuelve una promesa, puede asignarse a un const y consumirse desde otros bloques como si fuera un dato ya listo. La asincronía está, pero no la ves.
Generadores: valores que evolucionan
Cuando una variable necesita emitir una secuencia de valores en el tiempo (en lugar de un único valor que tarda en llegar), el patrón adecuado es un generador asíncrono. Cada yield empuja un nuevo valor al grafo, que reevalúa todo lo que depende de la variable. La Figura 3.4 construye un reloj vivo en seis líneas.
Ver código
const ahora = (async function* () {
while (true) {
yield new Date().toLocaleTimeString("es-MX");
await new Promise((r) => setTimeout(r, 1000));
}
})();
display(html`<p>⏱ ${ahora}</p>`);
Mutable: estado escrito desde el grafo
Promesas y generadores cubren los casos en que el valor cambia desde el bloque que lo define. Pero a veces necesitas lo contrario: que un bloque defina una variable y otro la modifique (pulsar un botón, registrar un clic, incrementar un contador). Para eso existe Mutable, un envoltorio que expone una propiedad .value escribible y notifica al grafo cuando cambia (Observable, Inc., 2026). La Figura 3.5 lo muestra en su forma más simple.
Mutable. Los botones invocan funciones que escriben en cuenta.value; el párrafo que muestra el número se reevalúa cada vez que el valor cambia, sin event listener explícito.Ver código
const cuenta = Mutable(0);
const incrementar = () => ++cuenta.value;
const reiniciar = () => (cuenta.value = 0);
display(Inputs.button([["Incrementar", incrementar], ["Reiniciar", reiniciar]]));
display(html`<p>Llevas <strong>${cuenta}</strong> pulsaciones.</p>`);
Dos detalles importan. Primero, los consumidores leen cuenta, no cuenta.value: el framework distingue entre la celda observable y su valor escrito. Segundo, solo el bloque que crea el Mutable debería escribir en él; otros bloques pueden hacerlo, pero la disciplina de mantener un único «autor» evita ciclos y comportamientos sorpresivos.
invalidation: limpiar lo que dejaste corriendo
Cuando un bloque arranca un timer, una conexión WebSocket o un event listener, el framework necesita una forma de cancelarlos cuando ese bloque se reevalúa o cuando el lector navega a otra página. La promesa especial invalidation, disponible en cualquier bloque, se resuelve justo en ese momento. La pauta es sencilla: invalidation.then(() => clearInterval(id)). Sin ella, los recursos siguen activos de forma indefinida y consumen memoria del navegador.
¿Qué patrón elegir?
La Tabla 3.2 sintetiza las tres herramientas y propone un criterio.
| Mecanismo | El valor cambia… | Quién lo escribe | Caso típico |
|---|---|---|---|
| Promesa | Una vez, cuando la operación termina. | El propio bloque, al resolverse. | Cargar un CSV, consultar una API, leer un archivo grande. |
| Generador asíncrono | Repetidamente, según un ritmo interno. | El propio bloque, en cada yield. |
Reloj, animación, polling, streaming. |
Mutable |
Cuando alguien le escribe. | Otro bloque, mediante .value =. |
Contador con botón, estado de UI, log de eventos del lector. |
Mutable resuelven problemas distintos. Identificar quién escribe el valor es la pregunta que más rápido te lleva al mecanismo correcto.Mutable, prueba view
Casi todo lo que parece requerir estado mutable se resuelve con un control reactivo y la función view que ya conoces. Mutable aparece en menos del cinco por ciento de las páginas reales: úsalo cuando el estado deba sobrevivir a interacciones de varios componentes, no para reaccionar a un solo control.
Markdown, datos y otros lenguajes en el mismo grafo
La reactividad no es exclusiva del JavaScript. Los data loaders del capítulo anterior se incorporan al grafo de forma natural: una llamada a FileAttachment("./data/fuentes.csv").csv() devuelve una promesa que el framework resuelve al construir la página, y desde ese momento la variable resultante es un nodo más del grafo. Si en el futuro reescribes el loader en R, en SQL o en shell, el resto de la página seguirá funcionando sin cambios: lo único que importa es el archivo de salida.
El framework también admite bloques cercados con otros lenguajes (sql, dot, tex, mermaid) que cohabitan con los js en el mismo documento. Una consulta sql puede consumir una variable JavaScript mediante interpolación y devolver una tabla que otro bloque js use para graficar. Un diagrama dot puede recibir las aristas como una cadena calculada en JavaScript. La frontera entre lenguajes es porosa: lo que viaja entre ellos son valores, no llamadas.
Incluso los atributos HTML aceptan expresiones en línea. Un fragmento como <div style="background: ${color}">… se vuelve a renderizar cuando color cambia. Esto convierte el Markdown en algo más que una plantilla: cada palabra, cada atributo y cada bloque pueden depender de los datos.
Cierre y puente al capítulo siguiente
Si reduces el capítulo a tres ideas, son estas. Primera: el orden de evaluación lo decide el grafo de dependencias, no el texto, lo cual te libera de coreografiar tus celdas. Segunda: las expresiones (en bloque o en línea) son la forma natural de mostrar valores; las declaraciones, la forma natural de alimentar el grafo. Tercera: el tiempo es un ciudadano de primera clase, y eligiendo bien entre promesa, generador y Mutable puedes modelar prácticamente cualquier interacción sin escribir un solo event listener.
El framework convierte un archivo Markdown en una hoja de cálculo textual: tú declaras relaciones entre valores, él se encarga del orden, de la propagación y de la limpieza. Pensar en relaciones, no en pasos, es el cambio de mentalidad que distingue al Markdown reactivo de cualquier script tradicional.
Con esto cubierto, el siguiente capítulo, Páginas, navegación y estructura, amplía la perspectiva: ya entiendes cómo vive una página reactiva por dentro; toca ver cómo se organizan muchas páginas en un sitio coherente, qué hace el observablehq.config.js por ti y cómo aprovechar el enrutamiento automático para proyectos más ambiciosos.