Anatomía de una app: ui + server + run
La app mínima: tres piezas
Cualquier app Shiny tiene exactamente tres componentes:
library(shiny)
# 1. ui — qué se ve en el navegador
ui <- fluidPage(
titlePanel("Mi primera app"),
sliderInput("n", "Número de muestras:", min = 10, max = 1000, value = 100),
plotOutput("histograma")
)
# 2. server — lógica que reacciona a los inputs
server <- function(input, output, session) {
output$histograma <- renderPlot({
hist(rnorm(input$n), main = "Distribución aleatoria")
})
}
# 3. shinyApp() — conecta ui y server y arranca el server
shinyApp(ui, server)Guárdalo como app.R y ejecuta. Se abre un navegador con la app. Mueve el slider, el histograma se redibuja.
Esto es Shiny en su esencia. Todo lo demás son refinamientos sobre estas tres piezas.
ui: lo que se ve
ui es un objeto de R que representa HTML. Lo que ves al inspeccionar ui en la consola es el HTML que se va a mandar al navegador del usuario.
fluidPage() es el contenedor más común. Hay otros:
fluidPage(): layout responsive básico.navbarPage(): con barra de navegación arriba (apps multi-página).dashboardPage()(deshinydashboard, legacy), layout tipo dashboard.page_sidebar(),page_navbar()(debslib, moderno), el reemplazo recomendado.
Dentro de la página pones componentes:
- Inputs:
sliderInput,selectInput,textInput,numericInput,dateInput… - Outputs:
plotOutput,tableOutput,textOutput,verbatimTextOutput… - Texto y HTML:
h1(),p(),tags$div(...), HTML directo. - Layout:
fluidRow(),column(),sidebarLayout()…
ui es declarativo: describes lo que quieres ver, no cómo dibujarlo.
server: lo que ocurre
server es una función con tres parámetros: input, output, session.
server <- function(input, output, session) {
# Lógica aquí
}Cada vez que un usuario abre tu app, esta función se ejecuta una vez para esa sesión, creando el “estado del servidor” para ese usuario específico. Dentro:
input$x: accede al valor actual del input con id"x".output$y: asigna una expresiónrender*()al output con id"y".session: objeto con metadatos de la sesión (id, token, datos del cliente).
Importante: server es una función. Si pones código fuera de ella (library(), carga de datos), ese código corre una vez al iniciar el servidor, antes de que ningún usuario se conecte. Si lo pones dentro, corre una vez por usuario.
shinyApp(ui, server): el conector
Esta función ensambla ui + server y arranca el servidor:
shinyApp(ui, server)En RStudio, al ejecutar el archivo app.R, el botón Run App detecta automáticamente la presencia de shinyApp() y lanza la app. Por línea de comandos:
shiny::runApp("ruta/al/proyecto/")runApp() busca un archivo app.R o un par ui.R + server.R en la carpeta y los corre.
Single-file vs multi-file
Dos estructuras habituales:
Single-file (app.R), todo en un archivo:
mi_app/
└── app.R
Recomendado para apps pequeñas (≤ 200 líneas). Más fácil de leer en un golpe de vista.
Multi-file, ui.R, server.R, opcionalmente global.R:
mi_app/
├── ui.R # solo el ui (sin asignar a variable)
├── server.R # solo el server (sin asignar)
└── global.R # código que corre ANTES y se comparte (libraries, datos)
Histórico, todavía soportado. Hoy se prefiere app.R único + archivos auxiliares en R/:
mi_app/
├── app.R
├── R/
│ ├── utils.R # funciones auxiliares
│ ├── module_ventas.R # módulos (cuando crezca)
│ └── module_clientes.R
├── www/ # assets estáticos (CSS, JS, imágenes)
└── data/ # datos
Shiny carga automáticamente todo lo que está en R/. Es el patrón moderno, el que usa golem (framework para apps Shiny en producción).
El modelo de runtime: una sesión R por usuario
Esto es importante entenderlo. Cuando un usuario abre tu app:
- Si es el primer usuario, R arranca el servidor Shiny.
- Shiny crea una nueva sesión R para ese usuario.
- La función
servercorre en esa sesión. - Cualquier
reactive(),observe()o estado vive solo en esa sesión.
Implicaciones críticas:
- Variables globales se comparten entre sesiones. Si modificas una variable global desde el server, todos los usuarios la ven.
inputyoutputson específicos por sesión. Cada usuario tiene su propioinput$slider.- Carga de datos dentro del server = una vez por usuario (lento si los datos son grandes). Fuera del server = una vez al arrancar = mucho más eficiente.
# Buena práctica: cargar datos UNA vez (fuera de server)
datos <- read_csv("data/grande.csv") # corre una vez
server <- function(input, output, session) {
# Aquí ya están disponibles para todos los usuarios sin recargar
output$tabla <- renderTable(datos)
}Desarrollo: hot-reload
En RStudio, Cmd/Ctrl + Shift + Enter en app.R lanza la app. Mientras está corriendo, si modificas el archivo y guardas, RStudio te ofrece relanzar la app automáticamente.
Para desarrollo más cómodo, activa shiny.autoreload:
options(shiny.autoreload = TRUE)
runApp()Con esto, cualquier cambio en cualquier archivo dispara auto-reload sin tocar el botón. Recomendado durante desarrollo.
Para debug, las tres herramientas básicas:
browser()dentro de unreactive()oobserve()te abre el debugger interactivo.message("xxx")imprime en la consola, útil para rastrear flujo reactivo.shiny::reactlog()(lo veremos en el tutorial de reactividad), visualiza el grafo reactivo entero.
Trampas habituales
serverque devuelve algo. La funciónserverno devuelve nada, todo lo que hace es asignar aoutput$xy crear reactividad. Si ponesreturn(x)al final, no ayuda.- Cargar datos dentro de
server. Para datos que no cambian entre usuarios, cargar fuera de la función server ahorra memoria y tiempo. Solo carga dentro si los datos son específicos de la sesión. - Variables globales mutables.
<<-desde el server modifica el entorno global y afecta a todos los usuarios. Casi siempre es un bug, no una feature. runApp()desde dentro de un script ejecutado en una sesión interactiva. A veces Shiny no detecta el contexto correctamente. Usa el botón Run App de RStudio o cierra explícitamente otras sesiones de Shiny corriendo.
En la siguiente entrega
Tienes la anatomía. La siguiente pieza son los componentes: cómo se conectan inputs y outputs, qué tipos hay y el patrón básico de binding inputId / outputId. Es lo siguiente.