Shiny
Framework reactivo de Posit para aplicaciones web en R
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 localTres ideas a interiorizar antes de escribir nada serio:
- El servidor no es un script lineal. Cada bloque
reactive(),observe()orender*()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()vsobserve()vseventReactive()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,
quartocon widgets dehtmlwidgetso 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()vsobserve()vseventReactive(). Diferencia central:reactive()produce un valor (lazy, cacheado por sesión),observe()produce efectos secundarios (eager),eventReactive()actualiza solo cuando se dispara uninputo 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")ymyModuleServer("id")con el mismoid. Toda app no trivial debería estar modulada. bindCache()/bindEvent(). Modificadores que añaden cacheo y control de disparo a unreactive. Sustituyen patrones antiguos comoreactiveValues+observeEventpara muchos casos.promises+futurehabilita 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$userDatavsreactiveValues()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: usarobserve()+reactiveValues()para algo que es unreactive()natural. El código crece, depura mal y reevalúa más de la cuenta. Si tu bloque devuelve un valor, es unreactive().- Variables globales mutables. Cualquier objeto definido fuera de
serveres compartido entre sesiones del mismo proceso. Si lo mutas, contaminas el estado de otros usuarios. UsareactiveValues()osession$userDatapara estado por sesión. - Outputs lentos sin
bindCache. Si unarender*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(). Llamarreactive(...)define la expresión. Usarla requiere paréntesis:mi_reactive(). Olvidarlo da errores opacos del tipo “object of class ‘reactiveExpr’ is not subsettable”.
Enlaces
- Página oficial
- Mastering Shiny, Hadley Wickham (libro online)
- Gallery oficial
- Componentes · Layouts
- Shiny for Python
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 confluidPagenishinydashboard. - 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
fluidPageoshinydashboardque 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):
shinydashboardsigue siendo más rápido para ese arquetipo concreto.
Conceptos clave
page_*como contenedor.page_fluid,page_sidebar,page_navbar,page_fillable. Sustituyen afluidPage/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 concolumn()yfluidRow().bs_theme(version = 5, bootswatch = "flatly")genera el tema. Variables Sass comoprimary,bg,fg,base_fontse 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
fluidPageconpage_*. 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, usacard().- Tematizado a base de
tags$style()inline. Vence al sistema. Si necesitas color custom, pásalo abs_theme()o define un fichero Sass.
Enlaces
Relacionados en esta página
shiny, framework base.bslibse 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
dashboardPagey 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:
bsliblo hace nativamente.shinydashboardrequiere 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()contabNameytabItems()+tabItem(tabName = ...)conectan la navegación de la sidebar con el contenido. La correspondencia portabNamees lo más confuso al principio.box()es el contenedor de contenido.valueBox()/infoBox()son las tarjetas de métrica de la cabecera.statusysolidHeadercontrolan 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
tabNamemal pareado. Si eltabNamedelmenuItemno coincide exactamente con el deltabItem, la pestaña aparece vacía sin error.- CSS roto al mezclar con
bslib. AdminLTE inyecta su propio CSS. Si dentro deshinydashboardmetes componentesbslib, los estilos chocan. No los mezcles. - Responsive limitado.
shinydashboardno es totalmente fluido en móviles. Si la app debe verse en pantallas pequeñas, mejorbslib.
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:
golemañ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
rhinoencaja 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()vsrun_app().run_dev()es para desarrollo (carga víadevtools::load_all(), modo verboso).run_app()es la entrada de producción.golem-config.ymlmaneja configuración por entorno (development, production) congolem::get_golem_config().- Tests con
testthat.golemrecomienda tests unitarios sobre la lógica (funciones puras) y tests de módulos contestServer(). Para tests end-to-end, parea conshinytest2.
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 usarshiny::runApp()a pelo. Pierdes el cargado víaload_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.ymles 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íarenv. Hacerlo a mano es propenso a errores.
Enlaces
Relacionados en esta página
shiny,golemes 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
rhinoimpone 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
golemo 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 delibrary().rhinoadopta el paqueteboxpara imports explícitos por archivo, evitando el problema de los namespaces globales.- Sass compilado.
app/styles/main.scssse compila a CSS. Linting constylelint. - Cypress para end-to-end. Tests de UI escritos en JavaScript, ejecutables en CI.
rhino.ymlcentraliza 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 CSSTrampas habituales
- Intentar mezclar el patrón
golemconrhino. Son visiones opuestas. Elige una. - Usar
library()en vez debox::use(). Funciona pero rompe la disciplina de imports explícitos que es parte del valor derhino. - No instalar Node. Sin Node/npm, parte del tooling (Cypress, stylelint, prettier) no arranca. Si no quieres Node,
rhinono es para ti. - Reescribir una app existente sin necesidad. La migración a
rhinodesde una app monolítica no es trivial. Hazlo solo si vas a quedarte mucho tiempo en ella.
Enlaces
Relacionados en esta página
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.jscontags$script(src = "script.js")yShiny.setInputValue()para comunicar de vuelta,shinyjsno 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/toggleaceptanidy opciones de animación (anim = TRUE,animType = "fade").disable/enablesobre 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 comojs$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 elns. Usasession$ns("panel")oNS(id, "panel")para resolverlo. hidden()solo oculta visualmente. El input sigue existiendo y enviando valor. Si lo que quieres es no enviar nada, usaconditionalPanelo lógica reactiva.
Enlaces
Relacionados en esta página
shiny,shinyjsextiende 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
shinytest2en GitHub Actions/GitLab CI antes de cada deploy. - Tests de módulos en aislamiento,
testServer()para lógica reactiva pura,shinytest2para 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
testthatdirecto sobre esas funciones, sin levantar la app.
Conceptos clave
AppDriveres 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 entests/testthat/_snaps/la primera vez. En ejecuciones posteriores compara con esa referencia. Cambios en la app requierentestthat::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()(deshiny, no deshinytest2) 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.
chromoteusa 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
maino en PR contramain.
Enlaces
Relacionados en esta página
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, modelosvetiver. 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.
renvpara fijar dependencias. Sin un lockfile, el despliegue puede romperse en cualquier momento. Usarenv::snapshot()y comprueba que tu plataforma de despliegue leerenv.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 enrenv.lockniDESCRIPTION. En local funciona. En el deploy, no. Ejecutarenv::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 comohere. - 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
renvo 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
- Font Awesome, usable directamente con
icon("name"). - Bootstrap Icons, recomendado con
bslib. Víabsicons::bs_icon("name").