Anatomía de una app: ui + server + run

r
shiny
Las tres piezas universales de cualquier app Shiny: ui (lo que se ve), server (lo que ocurre) y shinyApp() (el conector). Single-file vs app.R + global.R + ui.R + server.R, el modelo de runtime y desarrollo con hot-reload.

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() (de shinydashboard, legacy), layout tipo dashboard.
  • page_sidebar(), page_navbar() (de bslib, 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ón render*() 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:

  1. Si es el primer usuario, R arranca el servidor Shiny.
  2. Shiny crea una nueva sesión R para ese usuario.
  3. La función server corre en esa sesión.
  4. 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.
  • input y output son específicos por sesión. Cada usuario tiene su propio input$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 un reactive() o observe() 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

  • server que devuelve algo. La función server no devuelve nada, todo lo que hace es asignar a output$x y crear reactividad. Si pones return(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.