Reactividad: el corazón conceptual
El grafo reactivo: el modelo mental
Antes de tocar funciones, hay que entender cómo piensa Shiny.
Shiny construye internamente un grafo dirigido donde:
- Productores reactivos (
input$x,reactive({...})) son nodos que producen valores. - Consumidores reactivos (
output$y,observe({...})) son nodos que consumen valores. - Aristas se crean automáticamente: si un consumidor lee a un productor, Shiny lo anota.
Cuando un productor cambia (por ejemplo, el usuario mueve un slider y input$n se actualiza), Shiny invalida todos los consumidores que dependen de él. Los consumidores se vuelven a ejecutar, pero solo los necesarios.
Esto es lo que distingue a Shiny de Streamlit. Streamlit reejecuta el script entero. Shiny ejecuta solo los nodos del grafo afectados por el cambio.
reactive(): cálculo perezoso reutilizable
server <- function(input, output, session) {
# Compartir cálculo entre múltiples outputs
datos <- reactive({
set.seed(input$seed)
rnorm(input$n)
})
output$grafico <- renderPlot({
hist(datos())
})
output$resumen <- renderPrint({
summary(datos())
})
}Lectura:
datos <- reactive({...})declara un valor reactivo. La expresión no se ejecuta inmediatamente.datos()(con paréntesis) ejecuta la expresión y devuelve el valor. Sí, losreactive()se acceden como funciones.reactive()cachea: siinput$nyinput$seedno han cambiado,datos()devuelve el valor memoizado sin recalcular.
Tres propiedades clave de reactive():
- Lazy: solo se ejecuta cuando alguien lo invoca con
datos(). Si ningún output ni observer lo usa, nunca corre. - Cacheado: dentro de una invalidación, solo se ejecuta la primera vez. Las llamadas siguientes devuelven el valor cacheado.
- Devuelve valor: produce un resultado que otros consumidores usan. No tiene efectos secundarios.
observe(): efecto secundario reactivo
A veces no quieres un valor, quieres hacer algo cuando un input cambia:
server <- function(input, output, session) {
observe({
message("El usuario seleccionó: ", input$region)
})
}Diferencias con reactive():
- No es lazy: corre inmediatamente cuando se invalida, sin que nadie lo pida.
- No tiene cacheo: siempre se ejecuta entero cuando se invalida.
- No devuelve valor utilizable: su propósito es el side effect (log, escribir DB, actualizar UI vía
updateSelectInput, etc.).
Casos típicos:
- Escribir a log o consola para debug.
- Actualizar inputs en base a otros (
updateSelectInput()cuando cambia el dataset). - Escribir cambios a una base de datos.
- Mandar emails, lanzar notificaciones.
observeEvent(): explícito sobre el trigger
Cuando quieres responder a un evento específico (un click, un cambio concreto), observeEvent() es más claro que observe():
server <- function(input, output, session) {
observeEvent(input$recalcular, {
showNotification("Recalculando…")
# Aquí va la lógica que solo corre al pulsar el botón
})
}Conceptualmente: “observa input$recalcular. Cuando cambie, ejecuta esto”.
La diferencia con observe():
observe()reacciona a todos los inputs que lee en su cuerpo.observeEvent()reacciona solo al primer argumento (el “evento”). Los otros valores que el cuerpo lee se acceden conisolate()implícitamente, no disparan reejecución.
Ejemplo donde la diferencia importa:
# Con observe(): se ejecuta cuando CUALQUIERA de los dos cambia
observe({
if (input$boton > 0) {
procesa(input$valor)
}
})
# Con observeEvent(): solo se ejecuta cuando se PULSA el botón
observeEvent(input$boton, {
procesa(input$valor)
})Si el usuario cambia input$valor sin pulsar el botón, la primera versión se ejecuta y la segunda no. La segunda es probablemente lo que querías, “corre cuando el usuario decida explícitamente”.
observeEvent() es el patrón natural para botones (actionButton).
eventReactive(): valor solo cuando dispara
A veces quieres un valor que solo se actualiza cuando un evento dispara:
server <- function(input, output, session) {
resultado <- eventReactive(input$calcular, {
# Solo se recalcula cuando el botón se pulsa
set.seed(input$seed)
rnorm(input$n)
})
output$grafico <- renderPlot({
hist(resultado())
})
}eventReactive() es a reactive() lo que observeEvent() es a observe():
reactive()recalcula cuando cualquier dependencia cambia.eventReactive()recalcula solo cuando el evento dispara.
Caso de uso típico: cálculos pesados que el usuario activa explícitamente. “No quiero rehacer el modelo cada vez que el usuario mueve un slider. Solo cuando pulse el botón de Entrenar”.
Resumen: matriz de decisión
| Necesitas… | Lazy? | Devuelve valor? | Usa |
|---|---|---|---|
| Cálculo reutilizable que se actualiza con sus inputs | Sí | Sí | reactive() |
| Hacer algo cuando los inputs cambian | No | No | observe() |
| Hacer algo cuando UN evento específico dispara | No | No | observeEvent() |
| Cálculo que se actualiza solo cuando UN evento dispara | No (eager al disparar) | Sí | eventReactive() |
Regla mental:
- ¿Necesitas un valor o un efecto? →
reactive/eventReactivevsobserve/observeEvent. - ¿Reacciona a todo lo que lee, o solo a un trigger? → forma básica vs forma
event*.
isolate(): romper la dependencia
A veces quieres leer un valor reactivo sin que su cambio dispare reejecución. isolate() lo desactiva temporalmente:
observe({
# Reacciona cuando cambia input$x, pero NO cuando cambia input$y
resultado <- input$x + isolate(input$y)
message("Resultado: ", resultado)
})Útil para “observa A pero usa el último valor de B”. Es lo que hace observeEvent internamente, solo el primer argumento es la dependencia. El resto va aislado por defecto.
Para usos avanzados, isolate() permite controlar exactamente qué dispara qué, al precio de más complejidad. Empieza por preferir observeEvent() o eventReactive(). Usa isolate() solo cuando la lógica lo justifique.
Lazy evaluation: nadie corre nada hasta que se necesita
Una propiedad importante: un reactive() que nunca se invoca, nunca se ejecuta.
server <- function(input, output, session) {
caro <- reactive({
# Esta computación carísima nunca corre si nadie usa caro()
procesa_lento(input$datos)
})
output$tabla <- renderTable({
if (input$modo == "rapido") {
head(input$datos)
} else {
caro() # solo aquí dispara la computación
}
})
}Si el usuario nunca cambia input$modo a algo distinto de "rapido", caro() nunca se ejecuta. Esto permite optimizar estructurando los reactive() para que el coste solo se pague cuando se necesita.
observe() y observeEvent() son eager, corren cuando se invalidan, las llame alguien o no. Por eso son para efectos, no para valores.
Debug con reactlog
Cuando el grafo reactivo se complica, visualizarlo ayuda. reactlog lo hace:
library(reactlog)
options(shiny.reactlog = TRUE)
# Ejecuta tu app
runApp()
# Mientras la app está corriendo, en otra consola R:
reactlog_show()Esto abre un visualizador interactivo del grafo. Cada nodo es un reactive/observer/output. Las aristas muestran dependencias. Puedes ver qué se invalidó y por qué con cada cambio.
Casi siempre lo necesitarás cuando tu app “no recalcula lo que esperabas” o “recalcula demasiado”. Es la herramienta diagnóstica principal.
Trampas habituales
- Llamar
reactivesin paréntesis al usarlo.datoses el reactive.datos()es su valor. Si escribeshist(datos), da error porquedatoses una función, no un vector. reactive()en lugar deeventReactive()con botones. Si declarasdatos <- reactive({...})y el usuario quiere control explícito, cualquier cambio eninput$nrecalcula, no esperaría al botón. UsaeventReactive().observe()para producir valores que otros leen.observe()no devuelve nada utilizable. Si quieres compartir un valor, esreactive(). Confundir esto es uno de los errores conceptuales más comunes.- Dependencias accidentales por leer
input$xdentro de un reactive. Cualquierinput$xleído invalida el reactive. Si quieres “leer pero no reaccionar”, usaisolate(). La regla: lo que lees, te ata.
En la siguiente entrega
Has aprendido el modelo reactivo, el concepto más importante de Shiny. La siguiente entrega abre el Bloque 2 con layouts modernos: cómo organizar la UI con bslib (Bootstrap 5), cards, sidebars y navbars. Lo siguiente.