Shiny

Framework reactivo de Posit para aplicaciones web en R

r
shiny
web-apps
reactive
bslib
golem
rhino
shinytest2
deployment
Referencia comentada del ecosistema Shiny en R: framework reactivo (shiny), UI moderna (bslib, shinydashboard), arquitectura de aplicaciones de producción (golem, rhino), testing (shinytest2) y opciones de despliegue.

Sobre Shiny

Shiny es el framework web de facto del ecosistema R, mantenido por Posit (antes RStudio). Su propuesta de valor no es ser otro generador de HTML: es un modelo de programación reactivo en el que las dependencias entre inputs, expresiones intermedias y outputs se resuelven automáticamente, sin necesidad de cablear callbacks a mano. Esa abstracción permite que un analista escriba un dashboard interactivo decente en un par de cientos de líneas, y que un equipo de ingeniería levante una aplicación de producción con la misma base.

Instalación y arranque mínimo:

install.packages("shiny")

library(shiny)
runExample("01_hello")          # demo incluida
shiny::runApp("path/to/app")    # ejecutar una app local

Tres ideas a interiorizar antes de escribir nada serio:

  • El servidor no es un script lineal. Cada bloque reactive(), observe() o render*() se reevalúa cuando cambian sus dependencias. Pensar en términos de “primero esto, luego lo otro” lleva a bugs sutiles. Piensa en un grafo de dependencias, no en una secuencia.
  • reactive() vs observe() vs eventReactive() no son intercambiables. reactive() devuelve un valor cacheado. observe() ejecuta efectos secundarios. eventReactive() se dispara solo ante un trigger explícito. Confundirlos es la fuente número uno de aplicaciones lentas o con loops de actualización.
  • Modules son la única forma sostenible de escalar. A partir de unas 500-800 líneas, una app monolítica se vuelve inmantenible. Los módulos (moduleServer, NS) introducen namespacing y permiten reutilización real.

Desde 2022 existe Shiny for Python (shiny en PyPI), que reproduce el modelo reactivo en Python con sintaxis equivalente. Para proyectos nuevos en equipos mixtos R/Python, conviene evaluarlo frente a Streamlit o Dash. Aun así, en R Shiny sigue siendo claramente el estándar.

Esta página cataloga el ecosistema en orden conceptual: primero el framework (shiny), después la capa UI (bslib, shinydashboard), la arquitectura de aplicaciones de producción (golem, rhino), utilidades de cliente (shinyjs), testing (shinytest2) y finalmente despliegue.


shiny

shiny es el framework reactivo base. Define el contrato ui (función o objeto que devuelve un árbol de tags HTML) + server (función con firma function(input, output, session)), y la maquinaria reactiva que conecta ambos.

Desarrollado por Joe Cheng y el equipo de Posit. Estable, con API congelada en lo esencial desde hace años. Los cambios recientes son aditivos (bindCache, soporte de promises/async, integración con bslib).

Cuándo usarlo

  • Dashboards interactivos sobre datos en R donde el cálculo (modelo, query, plot) ya vive en R y no quieres reescribirlo en JavaScript.
  • Herramientas internas para equipos científicos o analíticos: aplicaciones de calidad de datos, exploradores de resultados, calculadoras de protocolo.
  • Productos donde el ciclo de iteración importa más que el rendimiento absoluto frontend: prototipar en horas, no días.

Cuándo NO usarlo

  • Sitios web públicos de alto tráfico. El modelo one-process-per-session de Shiny Server / Connect escala con dificultad más allá de unos cientos de sesiones concurrentes. Para eso, considera APIs en plumber + frontend SPA, o reescribir el front en Next.js/SvelteKit consumiendo una API.
  • Aplicaciones estáticas o casi-estáticas. Si lo que necesitas es un informe interactivo con un par de filtros, quarto con widgets de htmlwidgets o un dashboard de Quarto Dashboards suele ser más simple y barato de servir.
  • Equipos puramente Python. En ese caso, las alternativas son Streamlit (sencillo, modelo rerun-on-change, menos potente), Dash (más explícito, callbacks tipo Flask), o Shiny for Python si quieres el mismo modelo reactivo que R.
  • Dashboards de BI tradicionales (KPIs, slicers, drilldowns sobre cubos): Power BI, Tableau o Looker son mejor herramienta. Shiny brilla cuando el valor está en código, no en click-through.

Conceptos clave

  • reactive() vs observe() vs eventReactive(). Diferencia central: reactive() produce un valor (lazy, cacheado por sesión), observe() produce efectos secundarios (eager), eventReactive() actualiza solo cuando se dispara un input o expresión explícitos.
  • isolate() rompe la propagación reactiva dentro de un bloque, útil para leer un input sin que el bloque dependa de él. Mal usado, oculta bugs. Bien usado, evita reevaluaciones innecesarias.
  • Modules (NS, moduleServer). Encapsulan UI + lógica con su propio namespace. Llamada desde el padre: myModuleUI("id") y myModuleServer("id") con el mismo id. Toda app no trivial debería estar modulada.
  • bindCache() / bindEvent(). Modificadores que añaden cacheo y control de disparo a un reactive. Sustituyen patrones antiguos como reactiveValues + observeEvent para muchos casos.
  • promises + future habilita ejecución asíncrona dentro del server. Imprescindible si una sola query bloqueante puede congelar la app entera para todos los usuarios de ese worker.
  • session$userData vs reactiveValues() globales: lo primero es por sesión, lo segundo puede serlo o no. Cuidado con el scoping, variables a nivel de archivo son compartidas entre sesiones.

Patrón mínimo

library(shiny)

ui <- fluidPage(
  titlePanel("Demo reactiva"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("n", "Tamaño muestral", min = 10, max = 1000, value = 100),
      actionButton("go", "Recalcular")
    ),
    mainPanel(plotOutput("hist"))
  )
)

server <- function(input, output, session) {
  # Se dispara solo al pulsar el botón; ignora cambios en `n` hasta entonces
  draw <- eventReactive(input$go, {
    rnorm(input$n)
  }, ignoreNULL = FALSE)

  output$hist <- renderPlot({
    hist(draw(), col = "steelblue", main = NULL)
  })
}

shinyApp(ui, server)

Trampas habituales

  • observe() para calcular valores. Antipatrón clásico: usar observe() + reactiveValues() para algo que es un reactive() natural. El código crece, depura mal y reevalúa más de la cuenta. Si tu bloque devuelve un valor, es un reactive().
  • Variables globales mutables. Cualquier objeto definido fuera de server es compartido entre sesiones del mismo proceso. Si lo mutas, contaminas el estado de otros usuarios. Usa reactiveValues() o session$userData para estado por sesión.
  • Outputs lentos sin bindCache. Si una render* tarda segundos y cambia poco, cachéala. output$plot <- renderPlot({ ... }) |> bindCache(input$year, input$region) cambia drásticamente la experiencia.
  • Async mal entendido. promises::future_promise() no acelera por arte de magia: libera el worker para atender a otra sesión. Si tu app sirve a un usuario en local, async añade complejidad sin beneficio.
  • reactive() sin (). Llamar reactive(...) define la expresión. Usarla requiere paréntesis: mi_reactive(). Olvidarlo da errores opacos del tipo “object of class ‘reactiveExpr’ is not subsettable”.

Enlaces


bslib

bslib es el puente moderno entre Shiny y Bootstrap 5. Sustituye a los layouts y temas heredados (fluidPage, navbarPage con apariencia de 2014) por una API basada en cards, value boxes, sidebars y navsets, con tematizado declarativo vía variables Sass.

Desarrollado por Carson Sievert. Es la opción por defecto recomendada por Posit para aplicaciones nuevas desde 2023. No reemplaza shiny, convive con él añadiendo componentes UI más capaces.

Cuándo usarlo

  • Aplicación Shiny nueva: empieza directamente con bslib, no con fluidPage ni shinydashboard.
  • Necesitas un look-and-feel moderno y responsive sin escribir CSS.
  • Tematizado consistente entre Shiny app, Quarto doc y R Markdown report, bs_theme() es la pieza común.

Cuándo NO usarlo

  • Mantienes una app existente sobre fluidPage o shinydashboard que ya funciona. La migración tiene coste. Hazla solo si te aporta algo concreto (sidebars colapsables, dark mode, cards reactivas).
  • Necesitas un dashboard administrativo “clásico” (sidebar negra fija, métricas en la cabecera): shinydashboard sigue siendo más rápido para ese arquetipo concreto.

Conceptos clave

  • page_* como contenedor. page_fluid, page_sidebar, page_navbar, page_fillable. Sustituyen a fluidPage/navbarPage.
  • card(), value_box(), layout_columns(), layout_column_wrap(). Bloques de composición. layout_column_wrap(width = 1/3, ...) crea una rejilla responsive sin pelearse con column() y fluidRow().
  • bs_theme(version = 5, bootswatch = "flatly") genera el tema. Variables Sass como primary, bg, fg, base_font se pasan como argumentos.
  • bs_themer() abre un panel interactivo en la app para probar temas en caliente y exportar el resultado.
  • accordion(), navset_tab(), navset_card_tab(). Patrones UI estándar que evitan reinventar CSS.

Patrón mínimo

library(shiny)
library(bslib)

ui <- page_sidebar(
  title = "Demo bslib",
  theme = bs_theme(version = 5, bootswatch = "flatly"),
  sidebar = sidebar(
    sliderInput("n", "Muestras", 10, 1000, 100)
  ),
  layout_columns(
    value_box(
      title = "Media",
      value = textOutput("media"),
      showcase = bsicons::bs_icon("graph-up")
    ),
    card(
      card_header("Distribución"),
      plotOutput("hist")
    ),
    col_widths = c(4, 8)
  )
)

server <- function(input, output, session) {
  x <- reactive(rnorm(input$n))
  output$media <- renderText(sprintf("%.3f", mean(x())))
  output$hist  <- renderPlot(hist(x(), col = "steelblue", main = NULL))
}

shinyApp(ui, server)

Trampas habituales

  • Mezclar fluidPage con page_*. Funciona a medias pero da resultados inconsistentes. Decide la base y mantente.
  • Olvidar theme = bs_theme(version = 5). Sin ello, algunos componentes caen a Bootstrap 3 por compatibilidad y el resultado se ve a destiempo.
  • value_box() con texto plano largo. Está diseñado para una métrica corta. Para más contenido, usa card().
  • Tematizado a base de tags$style() inline. Vence al sistema. Si necesitas color custom, pásalo a bs_theme() o define un fichero Sass.

Enlaces

Relacionados en esta página

  • shiny, framework base. bslib se monta encima.
  • shinydashboard, alternativa para el patrón admin-dashboard clásico.

shinydashboard

shinydashboard implementa el arquetipo de dashboard administrativo: cabecera fija, sidebar oscura colapsable, cuerpo con boxes y infoBoxes. Maduro, estable y muy usado en aplicaciones internas de empresa.

Desarrollado originalmente por Winston Chang. Su sucesor recomendado para apps nuevas es bslib con page_sidebar() + value_box(), pero shinydashboard sigue siendo la vía más rápida para reproducir el patrón AdminLTE clásico cuando ése es justo el aspecto requerido.

Cuándo usarlo

  • Necesitas un dashboard interno con look corporativo “admin panel”, sidebar negra, header con título, boxes de métricas, y el equipo está cómodo con AdminLTE.
  • Migrar una app existente que ya usa dashboardPage y no tiene incentivo para reescribir UI.
  • shinydashboardPlus (extensión de RinteRface) si quieres componentes adicionales (timelines, gallery, social cards).

Cuándo NO usarlo

  • Proyecto nuevo en 2026: empieza con bslib. El resultado es más moderno, responsive y mantenible.
  • Necesitas tematizado fino o dark mode dinámico: bslib lo hace nativamente. shinydashboard requiere CSS manual.
  • App con UI poco “dashboard” (formularios, asistentes, calculadoras): el chrome de AdminLTE estorba.

Conceptos clave

  • dashboardPage(header, sidebar, body) estructura la app en tres bloques fijos.
  • menuItem() con tabName y tabItems() + tabItem(tabName = ...) conectan la navegación de la sidebar con el contenido. La correspondencia por tabName es lo más confuso al principio.
  • box() es el contenedor de contenido. valueBox() / infoBox() son las tarjetas de métrica de la cabecera.
  • status y solidHeader controlan el color de las cajas. Los colores son tokens de AdminLTE (“primary”, “success”, “warning”, “danger”…).

Patrón mínimo

library(shiny)
library(shinydashboard)

ui <- dashboardPage(
  dashboardHeader(title = "Mi panel"),
  dashboardSidebar(
    sidebarMenu(
      menuItem("Resumen",  tabName = "resumen",  icon = icon("dashboard")),
      menuItem("Detalle",  tabName = "detalle",  icon = icon("table"))
    )
  ),
  dashboardBody(
    tabItems(
      tabItem(tabName = "resumen",
        fluidRow(
          valueBox(42, "Usuarios activos", icon = icon("users")),
          box(title = "Tendencia", plotOutput("trend"))
        )
      ),
      tabItem(tabName = "detalle",
        box(title = "Datos", DT::dataTableOutput("tabla"), width = 12)
      )
    )
  )
)

server <- function(input, output, session) {
  output$trend <- renderPlot(plot(cumsum(rnorm(100)), type = "l"))
  output$tabla <- DT::renderDataTable(head(mtcars, 20))
}

shinyApp(ui, server)

Trampas habituales

  • tabName mal pareado. Si el tabName del menuItem no coincide exactamente con el del tabItem, la pestaña aparece vacía sin error.
  • CSS roto al mezclar con bslib. AdminLTE inyecta su propio CSS. Si dentro de shinydashboard metes componentes bslib, los estilos chocan. No los mezcles.
  • Responsive limitado. shinydashboard no es totalmente fluido en móviles. Si la app debe verse en pantallas pequeñas, mejor bslib.

Enlaces

Relacionados en esta página

  • bslib, alternativa moderna y recomendada para apps nuevas.

golem

golem es el framework opinado de Colin Fay (ThinkR) para empaquetar aplicaciones Shiny como paquetes R de producción. Convierte la app en un paquete con DESCRIPTION, NAMESPACE, R/, inst/app/www/, tests con testthat y un ciclo de desarrollo basado en devtools.

No es magia: lo que hace es codificar buenas prácticas (modularización, separación de UI/server/lógica de negocio, documentación, tests) en un esqueleto inicial y una serie de helpers (golem::add_module(), golem::add_utils(), golem::run_dev()).

Cuándo usarlo

  • Aplicación Shiny que va a producción: vas a desplegarla en Shiny Server, Connect, ShinyProxy o Docker, y quieres versionado, dependencias declaradas y tests.
  • Equipos de 2+ personas trabajando sobre la misma app: necesitas estructura compartida.
  • App con vida útil mayor a unos meses, con riesgo real de “código que nadie quiere tocar”.

Cuándo NO usarlo

  • Prototipo o herramienta interna efímera: golem añade un nivel de ceremonia que ralentiza la iteración inicial.
  • Aplicaciones que viven dentro de un proyecto más amplio (paquete de análisis con una app de demostración): la app puede ir directamente en inst/app/ sin todo el aparato.
  • Si rhino encaja mejor con tu equipo (más opinado, más basado en JavaScript/Sass moderno), valóralo como alternativa.

Conceptos clave

  • Estructura de paquete R. El proyecto es un paquete. run_app() es la función exportada que arranca la app. inst/app/www/ aloja recursos estáticos.
  • golem::add_module("nombre") crea un módulo Shiny (UI + server) con el boilerplate correcto.
  • golem::run_dev() vs run_app(). run_dev() es para desarrollo (carga vía devtools::load_all(), modo verboso). run_app() es la entrada de producción.
  • golem-config.yml maneja configuración por entorno (development, production) con golem::get_golem_config().
  • Tests con testthat. golem recomienda tests unitarios sobre la lógica (funciones puras) y tests de módulos con testServer(). Para tests end-to-end, parea con shinytest2.

Patrón mínimo

# Crear el esqueleto
golem::create_golem("miAppShiny")

# Dentro del proyecto: añadir un módulo
golem::add_module(name = "explorador")

# Lanzar en modo desarrollo
golem::run_dev()

# Construir el paquete y desplegar
devtools::document()
devtools::test()
devtools::build()

Trampas habituales

  • Saltarse run_dev() y usar shiny::runApp() a pelo. Pierdes el cargado vía load_all() y las opciones de entorno. Cuando despliegues, encontrarás diferencias.
  • Tocar UI sin pasar por módulos. El esqueleto invita a meter lógica directamente en app_ui.R / app_server.R. Resiste: todo lo que no sea trivial debe vivir en un módulo.
  • Configuración hardcoded. El propósito de golem-config.yml es no tener URLs, credenciales o paths fijados en el código. Aprovéchalo desde el día uno.
  • No ejecutar golem::add_dockerfile() antes de desplegar. Genera un Dockerfile con las dependencias congeladas vía renv. Hacerlo a mano es propenso a errores.

Enlaces

Relacionados en esta página

  • shiny, golem es una capa de ingeniería sobre Shiny.
  • rhino, alternativa con filosofía más opinada y front-end moderno.
  • shinytest2, testing end-to-end complementario.

rhino

rhino es el framework alternativo a golem desarrollado por Appsilon para aplicaciones Shiny de gran escala. Su filosofía es más opinada: estructura de carpetas fija, Sass y JavaScript modernos (ESLint, Prettier), tests unitarios con testthat + Cypress para end-to-end, CI/CD listo, y gestión de dependencias vía renv desde el inicio.

A diferencia de golem, no convierte la app en paquete R: la app es una app, no un paquete. Esto simplifica algunas cosas (no necesitas R/, NAMESPACE, roxygen) y complica otras (no hay devtools::install()).

Cuándo usarlo

  • App Shiny grande, en producción, mantenida por un equipo que incluye perfiles frontend o ingeniería de software (no solo data scientists en R).
  • Quieres Sass/JS modernos desde el inicio, con linting y formateo automatizados.
  • Te conviene la opinión fuerte sobre estructura: no quieres decidir dónde van las cosas.

Cuándo NO usarlo

  • El equipo es 100 % R y no toca JavaScript: el coste de aprender el flujo de Node/npm/Sass que rhino impone puede no compensar.
  • Necesitas que la app sea instalable como paquete R (por ejemplo, distribuirla por CRAN o un repo interno): para eso usa golem.
  • App pequeña o prototipo: la fricción inicial es mayor que con golem o Shiny “a pelo”.

Conceptos clave

  • Estructura fija: app/main.R (entrypoint), app/logic/ (lógica de negocio), app/view/ (UI y módulos), app/static/ (recursos), tests/ (testthat + Cypress).
  • box::use() en lugar de library(). rhino adopta el paquete box para imports explícitos por archivo, evitando el problema de los namespaces globales.
  • Sass compilado. app/styles/main.scss se compila a CSS. Linting con stylelint.
  • Cypress para end-to-end. Tests de UI escritos en JavaScript, ejecutables en CI.
  • rhino.yml centraliza configuración del framework (versión, build pipeline, opciones de linting).

Patrón mínimo

# Crear el proyecto
rhino::init("miAppRhino")

# Dentro del proyecto, en R:
rhino::app()                # arrancar en modo dev
rhino::test_r()             # tests R
rhino::test_e2e()           # tests Cypress (requiere Node)
rhino::lint_r()             # lintr sobre código R
rhino::lint_sass()          # stylelint sobre Sass
rhino::build_sass()         # compilar Sass a CSS

Trampas habituales

  • Intentar mezclar el patrón golem con rhino. Son visiones opuestas. Elige una.
  • Usar library() en vez de box::use(). Funciona pero rompe la disciplina de imports explícitos que es parte del valor de rhino.
  • No instalar Node. Sin Node/npm, parte del tooling (Cypress, stylelint, prettier) no arranca. Si no quieres Node, rhino no es para ti.
  • Reescribir una app existente sin necesidad. La migración a rhino desde una app monolítica no es trivial. Hazlo solo si vas a quedarte mucho tiempo en ella.

Enlaces

Relacionados en esta página

  • shiny, framework base.
  • golem, alternativa más afín al ecosistema R puro.

shinyjs

shinyjs es el paquete de Dean Attali que expone helpers de JavaScript al servidor Shiny sin que tengas que escribir JS: mostrar/ocultar elementos, deshabilitar/habilitar inputs, resetear formularios, ejecutar funciones JS arbitrarias, animar transiciones. Maduro, ampliamente usado y prácticamente obligatorio en cualquier app no trivial.

Cuándo usarlo

  • Necesitas manipular el DOM desde el server: shinyjs::hide("panel"), shinyjs::disable("submit"), shinyjs::toggle("avanzado").
  • Resetear un formulario completo: shinyjs::reset("form").
  • Ejecutar código JS puntual sin montar un fichero www/script.js: shinyjs::runjs("...").
  • Llamadas reutilizables a JS: shinyjs::extendShinyjs() registra funciones JS personalizadas.

Cuándo NO usarlo

  • Si vas a escribir mucho JavaScript propio, mejor un fichero www/script.js con tags$script(src = "script.js") y Shiny.setInputValue() para comunicar de vuelta, shinyjs no aporta a ese flujo.
  • La librería que necesitas ya tiene wrapper R en htmlwidgets: úsalo en vez de cablear JS a mano.

Conceptos clave

  • useShinyjs() en la UI. Sin esa llamada el paquete no inicializa. Es el error más frecuente.
  • hide / show / toggle aceptan id y opciones de animación (anim = TRUE, animType = "fade").
  • disable / enable sobre inputs. Útil para impedir doble submit.
  • runjs("código JS aquí") ejecuta JavaScript arbitrario en el cliente, en el contexto de la app.
  • extendShinyjs(script = "www/util.js", functions = c("foo")) registra funciones JS llamables desde R como js$foo(...).

Patrón mínimo

library(shiny)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  actionButton("toggle", "Mostrar / ocultar avanzado"),
  hidden(
    div(id = "avanzado",
      sliderInput("alpha", "Alpha", 0, 1, 0.5)
    )
  )
)

server <- function(input, output, session) {
  observeEvent(input$toggle, {
    shinyjs::toggle("avanzado", anim = TRUE)
  })
}

shinyApp(ui, server)

Trampas habituales

  • Olvidar useShinyjs() en la UI. Sin él, las funciones del paquete no hacen nada (a veces ni siquiera fallan ruidosamente).
  • Identificadores con namespaces. Dentro de un módulo, shinyjs::show("panel") no encuentra el id porque está prefijado por el ns. Usa session$ns("panel") o NS(id, "panel") para resolverlo.
  • hidden() solo oculta visualmente. El input sigue existiendo y enviando valor. Si lo que quieres es no enviar nada, usa conditionalPanel o lógica reactiva.

Enlaces

Relacionados en esta página

  • shiny, shinyjs extiende su capa de comunicación cliente-servidor.

shinytest2

shinytest2 es el framework oficial de Posit para testing end-to-end de aplicaciones Shiny. Sucesor de shinytest, ahora basado en chromote (Chrome DevTools Protocol) en vez de PhantomJS. Permite arrancar la app en un Chrome headless, simular interacciones (set_inputs, click), capturar el estado (inputs, outputs, valores reactivos, exportaciones explícitas) y compararlo con snapshots de referencia.

Imprescindible para apps en producción donde una regresión silenciosa puede costar caro.

Cuándo usarlo

  • App Shiny en producción que necesita protección frente a regresiones.
  • Pipelines de CI: ejecutar shinytest2 en GitHub Actions/GitLab CI antes de cada deploy.
  • Tests de módulos en aislamiento, testServer() para lógica reactiva pura, shinytest2 para integración real con DOM.

Cuándo NO usarlo

  • Prototipo o herramienta de un solo uso. Escribir tests E2E para una app que vive una semana no rinde.
  • Tests de lógica de negocio pura (funciones que transforman datos): mejor testthat directo sobre esas funciones, sin levantar la app.

Conceptos clave

  • AppDriver es la clase central. app <- AppDriver$new("app/") arranca la aplicación. app$set_inputs(n = 200), app$click("go"), app$get_values() interactúan con ella.
  • Snapshots. app$expect_values() guarda el estado actual en tests/testthat/_snaps/ la primera vez. En ejecuciones posteriores compara con esa referencia. Cambios en la app requieren testthat::snapshot_accept() para regenerar.
  • expect_screenshot(). Captura visual. Útil para detectar cambios de layout, pero también frágil ante mínimas variaciones de renderizado.
  • exports. Dentro del server, exportTestValues(...) expone valores reactivos internos al test, sin necesidad de hacerlos outputs reales.
  • testServer() (de shiny, no de shinytest2) es complementario: testea un módulo en aislamiento sin arrancar el navegador.

Patrón mínimo

# tests/testthat/test-app.R
library(shinytest2)

test_that("la app responde al slider", {
  app <- AppDriver$new(
    app_dir = test_path("../../"),   # raíz del proyecto
    name    = "demo",
    height  = 600,
    width   = 800
  )

  app$set_inputs(n = 500)
  app$click("go")

  # Comprueba snapshot de inputs + outputs
  app$expect_values()
})

Trampas habituales

  • Snapshots inestables. Si el output incluye fechas, ids generados, o muestras aleatorias sin set.seed(), el snapshot fallará en cada ejecución. Aísla la fuente de variabilidad o exclúyela del snapshot.
  • Tests demasiado holísticos. Un solo test que valida 30 interacciones es difícil de depurar cuando falla. Tests pequeños y específicos rinden más.
  • No fijar versiones del navegador en CI. chromote usa la versión de Chrome del sistema. Cambios entre versiones pueden alterar el render. Fija la versión en el runner.
  • Ejecutar tests E2E en cada commit. Son lentos. Sepáralos del pipeline rápido y ejecútalos solo en main o en PR contra main.

Enlaces

Relacionados en esta página

  • shiny, la app bajo test.
  • golem y rhino, ambos integran shinytest2 en su flujo recomendado.

Despliegue

Una vez la app funciona en local, hay que servirla. El abanico va desde el managed más simple hasta orquestación con Kubernetes. Las cuatro opciones realistas son shinyapps.io, Posit Connect, Shiny Server (open source o Pro) y ShinyProxy.

Cuándo usar cada opción

  • shinyapps.io: managed de Posit, gratis para apps pequeñas, planes pagos por uso. Ideal para prototipos, demos públicas y herramientas internas de bajo tráfico sin requisitos de despliegue propio. Límite duro: una worker instance sirve a múltiples sesiones del mismo proceso R. El nivel gratuito impone horas-activas mensuales.
  • Posit Connect: producto comercial on-prem o cloud privado. Sirve apps Shiny, APIs plumber, documentos Quarto/RMarkdown, modelos vetiver. Tiene autenticación corporativa (SSO), versionado, scheduling. La opción por defecto en organizaciones que ya usan Posit.
  • Shiny Server (open source): instalable en un VPS Linux propio. Gratis, sin autenticación nativa (delega a un reverse proxy con OAuth, por ejemplo). Tope práctico: unos pocos cientos de sesiones por nodo. Buena para entornos académicos o internos con presupuesto cero.
  • ShinyProxy: proyecto de Open Analytics. Orquesta una app por contenedor Docker por usuario. Escala mejor que Shiny Server porque aísla sesiones. Integra autenticación (LDAP, OAuth, OpenID). Requiere operar Docker (y normalmente Kubernetes para producción).

Conceptos clave

  • Modelo de proceso. Cada sesión Shiny vive en un proceso R compartido (Shiny Server open source) o en un contenedor (ShinyProxy). Esto determina el coste por usuario y la estrategia de aislamiento.
  • Memoria por sesión. El consumo crece con el número de sesiones concurrentes. En shinyapps.io, el tier gratuito limita a 1 GB por instancia.
  • renv para fijar dependencias. Sin un lockfile, el despliegue puede romperse en cualquier momento. Usa renv::snapshot() y comprueba que tu plataforma de despliegue lee renv.lock.
  • session$onSessionEnded(). Limpieza al cerrar sesión (cerrar conexiones, liberar archivos temporales). Crítico en producción.
  • Autenticación. Shiny no la trae nativa. Para auth seria: Posit Connect (incluido), ShinyProxy (configuración explícita), o un reverse proxy (nginx + OAuth2 Proxy, Cloudflare Access) delante de Shiny Server.

Patrón mínimo (rsconnect a shinyapps.io)

install.packages("rsconnect")

rsconnect::setAccountInfo(
  name   = "mi-cuenta",
  token  = "...",
  secret = "..."
)

rsconnect::deployApp(
  appDir   = "path/to/app",
  appName  = "mi-app",
  appFiles = NULL          # NULL = todo el directorio
)

Trampas habituales

  • Paquetes en library() pero no en renv.lock ni DESCRIPTION. En local funciona. En el deploy, no. Ejecuta renv::snapshot() antes de cada push serio.
  • Rutas absolutas. read.csv("/home/user/data.csv") rompe en el servidor. Usa rutas relativas al directorio de la app, o paquetes como here.
  • Datos pesados en el repo. shinyapps.io tiene límite de tamaño de bundle. Para datos grandes, sirve desde S3 / GCS / Posit Connect Content.
  • No medir tiempos de arranque. Si tu app tarda 40 segundos en levantar (carga modelos, descarga datos), la primera visita ve un timeout. Optimiza el arranque o usa startup scripts específicos del proveedor.
  • Mismatch de versión de R. shinyapps.io ofrece versiones concretas de R. Si desarrollas en una más nueva, el deploy puede fallar. Fija la versión a través de renv o configuración explícita.

Enlaces

Relacionados en esta página

  • golem, golem::add_dockerfile() prepara la imagen para ShinyProxy / Connect.
  • rhino, incluye plantillas de CI/CD para despliegue automatizado.

Recursos adicionales

Galería e inspiración

Utilidades de UI de Dean Attali

Conjunto de paquetes pequeños y muy usados de Dean Attali, todos integrables en cualquier app Shiny:

  • shinyalert, diálogos modales (alertas, confirmaciones) con SweetAlert.
  • shinyscreenshot, captura del estado actual de la app como imagen.
  • shinydisconnect, mensaje custom cuando la sesión cae.
  • shinybrowser, detecta navegador, resolución y dispositivo del cliente.
  • shinycssloaders, spinners de carga en outputs lentos.
  • shinyforms, formularios reutilizables con validación y persistencia.
  • colourpicker, selector de color como input Shiny.
  • timevis, timeline interactiva (wrapper de vis.js).

Iconografía