Estado persistente: bookmarking y reactiveValues
¿Qué quieres persistir?
Shiny por defecto es stateless por sesión: el usuario abre la app, la app empieza desde cero. Refrescas el navegador y pierdes todo el estado.
A veces esto basta. A veces no:
- “Compárteme el dashboard con estos filtros aplicados”: necesitas URL que restaura el estado.
- “Sigo el análisis donde lo dejé ayer”: necesitas persistencia entre sesiones.
- “El usuario añade entradas y se acumulan en una lista”: necesitas estado mutable dentro de la sesión.
Tres mecanismos distintos para tres necesidades distintas. Vamos uno por uno.
Bookmarking: URLs que restauran estado
Bookmarking convierte el estado actual de la app en una URL larga que, abierta de nuevo, restaura exactamente lo mismo. El usuario hace click en “Guardar”, copia la URL, la comparte, el receptor abre y ve la app como estaba.
Activarlo es una línea:
ui <- function(request) {
page_sidebar(
title = "Mi app",
sidebar = sidebar(
sliderInput("n", "Muestras:", min = 10, max = 1000, value = 100),
bookmarkButton()
),
plotOutput("grafico")
)
}
server <- function(input, output, session) {
output$grafico <- renderPlot({
hist(rnorm(input$n))
})
}
enableBookmarking(store = "url")
shinyApp(ui, server)Dos cambios respecto a una app normal:
uiahora es una función que reciberequest. Esto le permite a Shiny inyectar el estado restaurado.enableBookmarking(store = "url")antes delshinyApp().
Más bookmarkButton() dentro de la UI, el botón que el usuario pulsa para generar la URL.
Los dos modos: "url" vs "server"
enableBookmarking(store = "url") # estado codificado en la URL
enableBookmarking(store = "server") # estado guardado en el servidor, URL solo tiene un idModo "url":
- Estado serializado en query string (
?n=100®ion=Norte). - Funciona sin servidor de almacenamiento.
- La URL puede crecer mucho si hay muchos inputs.
- Buena privacidad: el servidor no guarda nada.
Modo "server":
- Estado guardado en disco del servidor. La URL solo tiene un id (
?_state_id_=abc123). - URL corta.
- Requiere persistencia en el host (los
shinyapps.iotiene plan dedicado). - El estado caduca (configurable).
Para apps con pocos inputs simples, usa "url". Para apps con mucho estado, usa "server" y configura caducidad.
reactiveValues() vs reactive()
reactive() produce un valor derivado de inputs/otros reactives, es read-only para los consumidores, no muta.
reactiveValues() crea un contenedor mutable que el server puede modificar y los consumidores lo leen reactivamente. Es lo que necesitas para estado que no viene de un input.
server <- function(input, output, session) {
# Contenedor mutable
estado <- reactiveValues(
historial = character(0),
contador = 0
)
# Modificar estado en respuesta a un evento
observeEvent(input$agregar, {
estado$historial <- c(estado$historial, input$nuevo)
estado$contador <- estado$contador + 1
})
# Leer estado reactivamente
output$lista <- renderUI({
tags$ul(lapply(estado$historial, tags$li))
})
output$total <- renderText({
paste("Total entradas:", estado$contador)
})
}Lo que reactiveValues() te da:
- Los campos (
estado$historial,estado$contador) son mutables. - Cualquier lectura es reactiva, modifica
estado$historial, los outputs que lo leen se actualizan. - Asignación dispara invalidación inmediata.
Casos de uso típicos:
- Acumular eventos: lista de cosas que el usuario añade en sesión.
- Estado de pasos: en una app multi-step, qué paso lleva el usuario.
- Caché temporal: resultado de un cálculo costoso que no quieres recalcular.
- Comunicación entre módulos: un módulo escribe, otros leen.
La diferencia clave: read-only vs mutable
# reactive(): no se puede mutar
datos <- reactive({ mtcars })
datos$cyl <- 999 # ERROR
# reactiveValues(): sí se puede
estado <- reactiveValues(datos = mtcars)
estado$datos$cyl <- 999 # OKRegla mental:
- Si el valor se deriva de otra cosa (filtros, cálculos), usa
reactive(). - Si el valor es estado en sí mismo (lo que el usuario ha hecho, contadores, listas acumuladas), usa
reactiveValues().
Confundirlos es uno de los errores más comunes de quien aprende reactividad, usar reactive() cuando necesita reactiveValues() y al revés.
Hooks onBookmark y onRestore
Si tu app tiene estado en reactiveValues(), bookmarking por defecto no lo guarda (solo guarda inputs). Para que sí lo haga:
server <- function(input, output, session) {
estado <- reactiveValues(historial = character(0))
# Al guardar bookmark, agrega estado custom
onBookmark(function(state) {
state$values$historial <- estado$historial
})
# Al restaurar bookmark, lee el estado custom
onRestore(function(state) {
estado$historial <- state$values$historial
})
# ... resto del server
}onBookmark() se ejecuta justo antes de generar la URL. state$values es donde guardas lo que quieras serializar.
onRestore() se ejecuta cuando el usuario abre una URL con bookmark. Recibe el state$values guardado y lo aplicas donde corresponda.
Excluir inputs del bookmark
A veces no quieres que ciertos inputs se guarden (tokens, campos sensibles):
server <- function(input, output, session) {
setBookmarkExclude(c("password", "csrf_token"))
}setBookmarkExclude() recibe los IDs a excluir. Bookmarking los ignora.
Persistencia real entre sesiones
Bookmarking guarda estado por URL. Para persistencia real entre sesiones (el usuario abre la app mañana y la encuentra como la dejó), necesitas almacenamiento externo:
library(DBI)
server <- function(input, output, session) {
# ID del usuario (por sesión, por cookie, por login)
user_id <- session$user %||% "anonymous"
# Cargar estado al inicio
estado <- reactiveValues(
favoritos = cargar_favoritos(user_id) # función custom contra DB
)
# Guardar cuando cambie
observeEvent(input$agregar_favorito, {
estado$favoritos <- c(estado$favoritos, input$nuevo)
guardar_favoritos(user_id, estado$favoritos) # escribe a DB
})
}Las opciones de almacenamiento típicas:
DBI+ SQLite/Postgres: para apps con varios usuarios.readr::write_csva un volumen persistente: para apps muy simples.pins(paquete): abstracción amigable para guardar/leer objetos R con versionado.
Para apps en producción con autenticación, normalmente el ID viene de session$user (con Posit Connect) o de un sistema de auth externo. Diseña la persistencia siempre alrededor de un identificador de usuario fiable, nunca por IP o cookie sola.
Patrón completo de ejemplo
App con bookmarking + estado mutable + persistencia entre sesiones:
library(shiny)
library(bslib)
ui <- function(request) {
page_sidebar(
title = "Lista de favoritos",
sidebar = sidebar(
textInput("nuevo", "Nuevo favorito:"),
actionButton("agregar", "Añadir", class = "btn-primary"),
hr(),
bookmarkButton()
),
card(
card_header("Tus favoritos"),
uiOutput("lista"),
textOutput("total")
)
)
}
server <- function(input, output, session) {
user_id <- session$user %||% "anonymous"
# Cargar al inicio (mock; en prod sería desde DB)
estado <- reactiveValues(
favoritos = readLines("favoritos.txt", warn = FALSE) %||% character(0)
)
# Añadir
observeEvent(input$agregar, {
req(input$nuevo != "")
estado$favoritos <- c(estado$favoritos, input$nuevo)
writeLines(estado$favoritos, "favoritos.txt")
updateTextInput(session, "nuevo", value = "")
showNotification("Añadido", type = "message", duration = 2)
})
# UI dinámica
output$lista <- renderUI({
if (length(estado$favoritos) == 0) {
em("Sin favoritos aún")
} else {
tags$ul(lapply(estado$favoritos, tags$li))
}
})
output$total <- renderText({
paste("Total:", length(estado$favoritos))
})
# Persistir en bookmark
onBookmark(function(state) {
state$values$favoritos <- estado$favoritos
})
onRestore(function(state) {
estado$favoritos <- state$values$favoritos
})
}
enableBookmarking(store = "url")
shinyApp(ui, server)Tres mecanismos integrados: estado mutable durante la sesión, bookmark URL para compartir, persistencia entre sesiones vía fichero.
Trampas habituales
uicomo objeto en lugar de función con bookmarking activado. Bookmarking requiereui <- function(request) { ... }, noui <- page_sidebar(...). Olvidarlo es la causa número uno de “bookmarking no funciona”.- Modificar
reactiveValuesdesde varios sitios sin orden claro. Si tresobserveEventdistintos modificanestado$xa la vez, el resultado depende del orden, caos. Centraliza la mutación en un solo sitio. reactive()cuando necesitasreactiveValues().reactive({ x <- 0; x <- x + 1; x })no mantiene estado entre invocaciones. Cada llamada empieza de cero. Para acumular,reactiveValues().- Persistir información sensible sin cifrado. Si guardas configuración de usuario en disco, no metas tokens, contraseñas o datos personales sin pensarlo. Considera tu modelo de amenaza.
En la siguiente entrega
Has aprendido a gestionar estado. Solo queda el último paso operativo: desplegar la app. Hay varias opciones según privacidad, escala y presupuesto. Lo siguiente.