| name | r-shiny |
| description | Expert Shiny app development in R - reactive programming, UI design, modules, performance, and security. Use when working with shiny, shinydashboard, or building interactive R web applications. Triggers on "dashboard interativo", "interactive dashboard", "dashboard em R", "R dashboard", "aplicativo R", "R app", "aplicação Shiny", "Shiny app", shiny library imports, server/ui function patterns like "ui <- fluidPage", "server <- function", reactive expressions, renderPlot, renderTable, observeEvent, reactiveVal, or user requests for Shiny help, web applications in R, or reactive programming guidance. |
| version | 1.1.0 |
| user-invocable | false |
| allowed-tools | Read, Write, Edit, Grep, Glob |
Expert Shiny Development in R
You are an expert Shiny developer following best practices from Hadley Wickham's "Mastering Shiny". This skill provides comprehensive guidance for building production-quality Shiny applications.
Core Shiny Architecture
Every Shiny app has three components:
- UI (User Interface): Visual layout using
fluidPage(), inputs, and outputs
- Server Function: Reactive logic connecting inputs to outputs
- shinyApp() Call: Launches the application
library(shiny)
ui <- fluidPage(
titlePanel("My App"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Sample size", 1, 100, 50)
),
mainPanel(
plotOutput("plot")
)
)
)
server <- function(input, output, session) {
output$plot <- renderPlot({
hist(rnorm(input$n))
})
}
shinyApp(ui, server)
Reactive Programming Principles
The Three Reactive Components
-
Reactive Sources (Inputs)
- User inputs:
input$name
- Read-only, reflects browser state
- Automatically invalidate dependents when changed
-
Reactive Conductors (Expressions)
- Created with
reactive({})
- Lazy and cached - only compute when needed
- Called like functions:
filtered_data()
- Use for expensive computations used multiple times
filtered <- reactive({
data |> filter(category == input$category)
})
output$plot <- renderPlot({ plot(filtered()) })
output$table <- renderTable({ head(filtered()) })
- Reactive Endpoints (Observers & Outputs)
- Outputs:
output$name <- render*()
- Observers:
observe({}), observeEvent()
- Eager and forgetful - run immediately when invalidated
- Use for side effects (logging, file writes, external updates)
Key Reactive Patterns
Pattern 1: Input Validation
output$plot <- renderPlot({
req(input$file)
data <- read.csv(input$file$datapath)
plot(data)
})
output$summary <- renderPrint({
validate(
need(input$n > 0, "Sample size must be positive"),
need(input$n <= 1000, "Sample size too large")
)
rnorm(input$n)
})
Pattern 2: Event-Driven Reactivity
results <- eventReactive(input$run_button, {
expensive_computation(input$params)
})
observeEvent(input$save_button, {
saveRDS(current_data(), "output.rds")
showNotification("Data saved!")
})
Pattern 3: Isolating Dependencies
observe({
isolate({
value <- input$value
})
process(input$trigger, value)
})
Pattern 4: Manual Reactive Values
counter <- reactiveVal(0)
observeEvent(input$increment, {
counter(counter() + 1)
})
output$count <- renderText({
counter()
})
UI Design Patterns
Layout Structures
Single-Page Layouts
fluidPage(
titlePanel("Title"),
sidebarLayout(
sidebarPanel(
),
mainPanel(
)
)
)
fluidPage(
fluidRow(
column(4, "Sidebar content"),
column(8, "Main content")
),
fluidRow(
column(6, "Left half"),
column(6, "Right half")
)
)
Multi-Page Layouts
navbarPage("App Title",
tabPanel("Analysis",
),
tabPanel("Results",
),
navbarMenu("More",
tabPanel("About"),
tabPanel("Help")
)
)
Common Input Controls
textInput("name", "Name", value = "", placeholder = "Enter name")
textAreaInput("comments", "Comments", rows = 3)
numericInput("age", "Age", value = 25, min = 0, max = 120)
sliderInput("height", "Height", min = 0, max = 250, value = 170)
selectInput("state", "State", choices = state.name)
radioButtons("type", "Type", choices = c("A", "B", "C"))
checkboxGroupInput("options", "Options", choices = c("X", "Y", "Z"))
dateInput("start", "Start Date")
dateRangeInput("period", "Period")
fileInput("upload", "Upload File", accept = c(".csv", ".xlsx"))
actionButton("run", "Run Analysis", class = "btn-primary")
Common Output Types
textOutput("message")
verbatimTextOutput("code")
tableOutput("static_table")
dataTableOutput("data_table")
plotOutput("plot")
plotOutput("plot",
click = "plot_click",
brush = "plot_brush",
hover = "plot_hover"
)
downloadButton("download", "Download Data")
Dynamic UI Patterns
Update Existing Inputs
observeEvent(input$reset, {
updateSliderInput(session, "n", value = 50)
updateSelectInput(session, "category", selected = "All")
})
observeEvent(input$country, {
cities <- get_cities(input$country)
updateSelectInput(session, "city", choices = cities)
})
Generate UI Programmatically
output$dynamic_inputs <- renderUI({
n <- input$num_vars
lapply(1:n, function(i) {
numericInput(paste0("var_", i), paste("Variable", i), value = 0)
})
})
observe({
values <- sapply(1:input$num_vars, function(i) {
input[[paste0("var_", i)]]
})
})
Conditional Panels
conditionalPanel(
condition = "input.type == 'advanced'",
sliderInput("detail", "Detail Level", 1, 10, 5)
)
Shiny Modules
Modules create reusable, isolated components with namespaced IDs.
Module Structure
mod_analysis_ui <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("variable"), "Variable", choices = NULL),
plotOutput(ns("plot"))
)
}
mod_analysis_server <- function(id, data) {
moduleServer(id, function(input, output, session) {
observe({
updateSelectInput(session, "variable", choices = names(data()))
})
output$plot <- renderPlot({
req(input$variable)
hist(data()[[input$variable]])
})
return(reactive({
input$variable
}))
})
}
ui <- fluidPage(
mod_analysis_ui("analysis1"),
mod_analysis_ui("analysis2")
)
server <- function(input, output, session) {
data <- reactive({ mtcars })
selected1 <- mod_analysis_server("analysis1", data)
selected2 <- mod_analysis_server("analysis2", data)
}
Module Best Practices
- Validate inputs with
stopifnot()
mod_server <- function(id, data) {
stopifnot(is.reactive(data))
moduleServer(id, function(input, output, session) {
})
}
- Return reactive values or lists of reactives
return(list(
selected = reactive({ input$choice }),
filtered = reactive({ filter_data() })
))
- Document module interfaces clearly
User Feedback Patterns
Validation and Error Handling
output$result <- renderText({
validate(
need(input$file, "Please upload a file"),
need(nrow(data()) > 0, "File is empty")
)
analyze(data())
})
library(shinyFeedback)
observeEvent(input$email, {
if (grepl("@", input$email)) {
feedbackSuccess("email", "Valid email")
} else {
feedbackDanger("email", "Invalid email format")
}
})
Notifications and Progress
showNotification("Analysis complete!", type = "message")
showNotification("Error occurred", type = "error", duration = 10)
observeEvent(input$run, {
withProgress(message = "Processing...", {
for (i in 1:10) {
incProgress(1/10, detail = paste("Step", i))
Sys.sleep(0.5)
}
})
})
Modal Dialogs
observeEvent(input$delete, {
showModal(modalDialog(
title = "Confirm Deletion",
"Are you sure you want to delete this data?",
footer = tagList(
modalButton("Cancel"),
actionButton("confirm_delete", "Delete", class = "btn-danger")
)
))
})
observeEvent(input$confirm_delete, {
removeModal()
})
File Handling
File Uploads
fileInput("upload", "Upload CSV", accept = ".csv")
data <- reactive({
req(input$upload)
read.csv(input$upload$datapath)
})
File Downloads
downloadButton("download_data", "Download")
output$download_data <- downloadHandler(
filename = function() {
paste0("data-", Sys.Date(), ".csv")
},
content = function(file) {
write.csv(filtered_data(), file, row.names = FALSE)
}
)
Interactive Graphics
Click, Hover, and Brush
plotOutput("plot",
click = "plot_click",
brush = "plot_brush",
hover = "plot_hover"
)
observeEvent(input$plot_click, {
selected <- nearPoints(data(), input$plot_click, xvar = "x", yvar = "y")
output$details <- renderPrint({ selected })
})
selected_data <- reactive({
brushedPoints(data(), input$plot_brush)
})
Tidy Evaluation in Shiny
When users select column names dynamically, use tidy evaluation:
filtered <- reactive({
data() |>
filter(.data[[input$filter_col]] > input$threshold)
})
output$plot <- renderPlot({
ggplot(data(), aes(x = .data[[input$x_var]], y = .data[[input$y_var]])) +
geom_point()
})
selected_cols <- reactive({
data() |> select(all_of(input$columns))
})
Performance Optimization
Caching with bindCache()
expensive_result <- reactive({
expensive_function(input$param1, input$param2)
}) |> bindCache(input$param1, input$param2)
output$plot <- renderPlot({
plot(complex_data())
}) |> bindCache(input$dataset, input$options)
Preprocessing Data
large_dataset <- read_rds("data/large_file.rds")
server <- function(input, output, session) {
filtered <- reactive({
large_dataset |> filter(category == input$cat)
})
}
Conditional Computation with Tabs
output$expensive_plot <- renderPlot({
req(input$tabs == "analysis")
complex_visualization()
})
Security Best Practices
Input Validation
data <- reactive({
req(input$file)
ext <- tools::file_ext(input$file$name)
validate(need(ext == "csv", "Please upload a CSV file"))
validate(need(input$file$size < 10e6, "File too large (max 10MB)"))
read.csv(input$file$datapath)
})
Avoiding Code Injection
allowed_formulas <- c("y ~ x", "y ~ x + z")
validate(need(input$formula %in% allowed_formulas, "Invalid formula"))
message <- glue_safe("Hello {input$name}")
dbGetQuery(con, "SELECT * FROM users WHERE id = ?", params = list(input$user_id))
Credential Management
con <- dbConnect(
host = Sys.getenv("DB_HOST"),
user = Sys.getenv("DB_USER"),
password = Sys.getenv("DB_PASSWORD")
)
library(config)
db <- config::get("database")
con <- dbConnect(host = db$host, user = db$user, password = db$password)
Testing
Testing Non-Reactive Functions
test_that("data cleaning works", {
raw <- data.frame(x = c(1, NA, 3))
cleaned <- clean_data(raw)
expect_equal(nrow(cleaned), 2)
expect_false(any(is.na(cleaned$x)))
})
Testing Server Logic
testServer(server, {
session$setInputs(n = 50)
expect_equal(output$mean, 50)
session$setInputs(category = "A")
expect_gt(nrow(filtered()), 0)
})
testServer(mod_analysis_server, args = list(data = reactive(mtcars)), {
session$setInputs(variable = "mpg")
expect_true(inherits(output$plot, "shiny.render.function"))
})
Code Organization
File Structure
Small apps (< 200 lines):
app.R
Medium apps (200-500 lines):
app.R
R/
utils.R
ui.R
server.R
Large apps (> 500 lines):
app.R or R/run.R
R/
mod_*.R # One file per module
utils.R
ui_helpers.R
data_processing.R
DESCRIPTION # Optional: package structure
Extracting Functions
filtered_data <- reactive({
raw_data() |>
filter(date >= input$start, date <= input$end) |>
mutate(
category = case_when(
value < 10 ~ "low",
value < 50 ~ "medium",
TRUE ~ "high"
)
) |>
group_by(category) |>
summarize(
mean = mean(value),
sd = sd(value),
n = n()
)
})
categorize_and_summarize <- function(data, start_date, end_date) {
data |>
filter(date >= start_date, date <= end_date) |>
mutate(
category = case_when(
value < 10 ~ "low",
value < 50 ~ "medium",
TRUE ~ "high"
)
) |>
group_by(category) |>
summarize(
mean = mean(value),
sd = sd(value),
n = n()
)
}
filtered_data <- reactive({
categorize_and_summarize(raw_data(), input$start, input$end)
})
Reference Documentation
For detailed information on specific topics, see:
Code Templates
Working Examples
Remember: Keep reactivity in the server function, put complex computation in regular functions, test thoroughly, validate all user inputs, and optimize based on profiling results.