flowchart TB subgraph "Browser (Client-Side)" A[User Interface - UI] A1[Input Controls] A2[Output Displays] A3[Layout Structure] A --> A1 A --> A2 A --> A3 end subgraph "R Session (Server-Side)" B[Server Function] B1[Reactive Expressions] B2[Render Functions] B3[Observer Functions] B --> B1 B --> B2 B --> B3 end subgraph "Shiny Framework" C[Reactive System] C1[Input Binding] C2[Output Binding] C3[Session Management] C --> C1 C --> C2 C --> C3 end A1 -->|User Input| C1 C1 --> B1 B2 --> C2 C2 -->|Updated Output| A2 style A fill:#e1f5fe style B fill:#e8f5e8 style C fill:#fff3e0
Key Takeaways
- Two-Component Architecture: Every Shiny app consists of UI (user interface) and Server (computational logic) working in harmony
- Reactive Data Flow: Information flows from UI inputs through reactive expressions to server outputs in an automatic, efficient cycle
- Session Management: Each user connection creates an isolated session with its own input/output state and server environment
- Modular Design: Components can be organized into reusable modules for scalable, maintainable application development
- Separation of Concerns: UI handles presentation and user interaction while Server manages data processing and business logic
Introduction
Understanding Shiny’s application architecture is crucial for building robust, scalable applications that perform well and are easy to maintain. Unlike traditional web applications that require separate frontend and backend technologies, Shiny’s unique architecture integrates both presentation and logic layers within a single R-based framework.
This comprehensive guide explores how Shiny applications are structured internally, how components communicate with each other, and how the reactive programming model creates seamless user experiences. Whether you’re debugging application behavior, optimizing performance, or designing complex applications, understanding these architectural principles will make you a more effective Shiny developer.
App Structure Cheatsheet - All the patterns from this tutorial condensed into a scannable reference guide with essential code snippets.
Instant Reference • Ready-to-Use Code • Mobile-Friendly
The Foundation: UI-Server Architecture
Shiny’s architecture is built on a fundamental separation between presentation (UI) and logic (Server), connected through a sophisticated reactive system that manages data flow and updates.
The UI component defines everything users see and interact with in their browser:
Input Controls:
- Form elements (sliders, dropdowns, text inputs)
- Action triggers (buttons, links)
- File upload interfaces
- Custom input widgets
Output Displays:
- Dynamic content areas (plots, tables, text)
- Formatted HTML elements
- Download handlers
- Custom HTML widgets
Layout Structure:
- Page organization and navigation
- Responsive design elements
- Styling and theming
- Modal dialogs and notifications
Example UI Structure:
<- fluidPage(
ui # Application header
titlePanel("Data Analysis Dashboard"),
# Layout structure
sidebarLayout(
# Input controls section
sidebarPanel(
selectInput("dataset", "Choose Dataset:",
choices = c("mtcars", "iris")),
sliderInput("bins", "Number of bins:",
min = 1, max = 50, value = 30)
),
# Output display section
mainPanel(
tabsetPanel(
tabPanel("Plot", plotOutput("histogram")),
tabPanel("Summary", verbatimTextOutput("summary")),
tabPanel("Data", tableOutput("data"))
)
)
) )
The server function contains all computational logic and manages the application’s behavior:
Core Responsibilities:
- Processing user inputs and generating outputs
- Managing data transformations and analysis
- Handling reactive dependencies and updates
- Maintaining application state and session data
Key Functions:
reactive()
expressions for reusable computationsrender*()
functions for generating outputsobserve()
functions for side effectsreq()
for input validation and flow control
Example Server Structure:
<- function(input, output, session) {
server
# Reactive expression for data processing
<- reactive({
selected_data switch(input$dataset,
"mtcars" = mtcars,
"iris" = iris)
})
# Output rendering
$histogram <- renderPlot({
output<- selected_data()
data hist(data[[1]], breaks = input$bins,
main = paste("Histogram of", names(data)[1]))
})
$summary <- renderPrint({
outputsummary(selected_data())
})
$data <- renderTable({
outputhead(selected_data(), 100)
}) }
Understanding how Shiny applications are structured becomes much clearer when you can build and manipulate layouts visually:
The relationship between UI functions, layout components, and the final visual result can be confusing when working only with code. Interactive layout construction helps bridge this gap between code concepts and visual outcomes.
Visualize app structure using our Interactive Layout Builder →
Experiment with different layout patterns, see how UI components fit together, and understand the relationship between layout functions and actual visual results - making app structure concepts concrete and actionable.
Explore Shiny Architecture Components
Master the UI-Server relationship through hands-on exploration:
- Add input controls - Create sliders, dropdowns, and buttons to see UI structure
- Modify server logic - Change reactive expressions and observe behavior
- Watch data flow - See how inputs trigger server computations
- Test reactivity - Experience automatic updates in real-time
Key Learning: Understanding the clear separation between presentation (UI) and logic (Server) is fundamental to building maintainable Shiny applications.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 900
#| editorHeight: 300
library(shiny)
library(bslib)
library(bsicons)
ui <- page_sidebar(
title = "Shiny Architecture Explorer",
theme = bs_theme(version = 5, bootswatch = "cosmo"),
sidebar = sidebar(
width = 300,
card(
card_header(
"UI Components",
bs_icon("layout-sidebar")
),
# Dynamic input controls
selectInput("input_type", "Add Input Control:",
choices = c("Select One" = "",
"Slider" = "slider",
"Select Box" = "select",
"Text Input" = "text",
"Numeric Input" = "numeric")),
conditionalPanel(
condition = "input.input_type == 'slider'",
sliderInput("demo_slider", "Sample Slider:",
min = 0, max = 100, value = 50)
),
conditionalPanel(
condition = "input.input_type == 'select'",
selectInput("demo_select", "Sample Select:",
choices = c("Option A", "Option B", "Option C"))
),
conditionalPanel(
condition = "input.input_type == 'text'",
textInput("demo_text", "Sample Text:", value = "Hello Shiny!")
),
conditionalPanel(
condition = "input.input_type == 'numeric'",
numericInput("demo_numeric", "Sample Numeric:",
value = 42, min = 0, max = 100)
)
),
card(
card_header(
"Server Logic",
bs_icon("cpu")
),
checkboxInput("show_reactive", "Show Reactive Expression", TRUE),
checkboxInput("show_observer", "Show Observer Function", FALSE),
checkboxInput("show_validation", "Add Input Validation", FALSE),
sliderInput("update_delay", "Reactive Update Delay (ms):",
min = 0, max = 2000, value = 500, step = 100)
)
),
# Main content area
layout_columns(
col_widths = c(6, 6),
card(
card_header(
"Live UI Output",
bs_icon("display")
),
card_body(
conditionalPanel(
condition = "input.input_type != ''",
div(
class = "alert alert-info",
style = "margin-bottom: 15px;",
h6("Current Input Value:"),
verbatimTextOutput("current_value", placeholder = TRUE)
)
),
div(
h6("Reactive Output:"),
conditionalPanel(
condition = "input.show_reactive",
plotOutput("demo_plot", height = "200px")
),
conditionalPanel(
condition = "!input.show_reactive",
div(
class = "text-muted text-center p-4",
bs_icon("graph-up", size = "2em"),
br(),
"Enable 'Show Reactive Expression' to see output"
)
)
)
)
),
card(
card_header(
"Architecture Analysis",
bs_icon("diagram-3")
),
card_body(
h6("Component Flow:"),
verbatimTextOutput("architecture_flow"),
hr(),
h6("Reactive Dependencies:"),
verbatimTextOutput("dependencies"),
conditionalPanel(
condition = "input.show_observer",
hr(),
h6("Observer Log:"),
verbatimTextOutput("observer_log")
)
)
)
)
)
server <- function(input, output, session) {
# Reactive values for tracking
values <- reactiveValues(
observer_count = 0,
last_update = NULL
)
# Get current input value based on type
current_input <- reactive({
req(input$input_type)
# Add artificial delay if specified
if (input$update_delay > 0) {
Sys.sleep(input$update_delay / 1000)
}
switch(input$input_type,
"slider" = input$demo_slider,
"select" = input$demo_select,
"text" = input$demo_text,
"numeric" = input$demo_numeric,
"No input selected")
})
# Validation reactive
validated_input <- reactive({
req(input$input_type)
value <- current_input()
if (input$show_validation) {
validate(
need(input$input_type != "", "Please select an input type"),
need(!is.null(value), "Input value cannot be empty")
)
if (input$input_type == "numeric" && is.numeric(value)) {
validate(need(value >= 0, "Numeric input must be non-negative"))
}
}
value
})
# Observer for tracking
observeEvent(current_input(), {
if (input$show_observer) {
values$observer_count <- values$observer_count + 1
values$last_update <- Sys.time()
}
})
# Outputs
output$current_value <- renderText({
if (input$input_type == "") return("Select an input type above")
if (input$show_validation) {
validated_input()
} else {
current_input()
}
})
output$demo_plot <- renderPlot({
req(input$input_type)
value <- if (input$show_validation) validated_input() else current_input()
if (input$input_type == "slider" || input$input_type == "numeric") {
# Numeric plot
x <- 1:10
y <- x * as.numeric(value) / 10
plot(x, y, type = "b", col = "steelblue", lwd = 2,
main = paste("Reactive Plot (multiplier:", value, ")"),
xlab = "X", ylab = "Y")
} else {
# Text-based plot
barplot(c(nchar(as.character(value)), 10 - nchar(as.character(value))),
names.arg = c("Input Length", "Remaining"),
col = c("steelblue", "lightgray"),
main = paste("Text Analysis:", value))
}
})
output$architecture_flow <- renderText({
if (input$input_type == "") {
return("1. Select an input type to see data flow\n2. UI → Server communication will be demonstrated\n3. Reactive expressions will be triggered")
}
flow <- paste(
"1. User interacts with:", input$input_type, "input",
"\n2. Browser sends value to Shiny server",
"\n3. Server reactive expression processes:", class(current_input())[1],
if (input$show_validation) "\n4. Validation checks applied" else "",
"\n", if (input$show_validation) "5." else "4.", "Output updated in UI",
"\n", if (input$show_validation) "6." else "5.", "User sees result in browser"
)
flow
})
output$dependencies <- renderText({
deps <- paste(
"current_input() depends on:",
"\n • input$input_type",
"\n • input$", switch(input$input_type %||% "none",
"slider" = "demo_slider",
"select" = "demo_select",
"text" = "demo_text",
"numeric" = "demo_numeric",
"[none]"),
"\n • input$update_delay",
if (input$show_validation) "\n\nvalidated_input() depends on:\n • current_input()\n • input$show_validation" else "",
"\n\nOutputs depend on:",
if (input$show_validation) "\n • validated_input()" else "\n • current_input()"
)
deps
})
output$observer_log <- renderText({
if (!input$show_observer) return("")
paste(
"Observer executions:", values$observer_count,
"\nLast update:", if (!is.null(values$last_update)) format(values$last_update, "%H:%M:%S") else "None"
)
})
}
shinyApp(ui, server)
Input Controls Cheatsheet - Copy-paste code snippets, validation patterns, and essential input widget syntax.
Instant Reference • All Widget Types • Mobile-Friendly
The Reactive System: Heart of Shiny Architecture
The reactive system is what makes Shiny applications feel alive and responsive. It automatically manages dependencies between inputs and outputs, ensuring efficient updates when data changes.
Reactive Programming Principles
Lazy Evaluation:
- Reactive expressions only execute when their results are needed
- Automatic caching prevents unnecessary recalculations
- Dependencies are tracked automatically
Automatic Invalidation:
- When inputs change, dependent outputs are marked for update
- Updates propagate through the dependency graph
- Only affected components are recalculated
flowchart LR subgraph "Input Layer" I1[input$dataset] I2[input$bins] end subgraph "Reactive Layer" R1[selected_data] R2[processed_data] end subgraph "Output Layer" O1[output$histogram] O2[output$summary] O3[output$data] end I1 --> R1 R1 --> R2 R1 --> O2 R1 --> O3 R2 --> O1 I2 --> O1 style I1 fill:#ffebee style I2 fill:#ffebee style R1 fill:#f3e5f5 style R2 fill:#f3e5f5 style O1 fill:#e8f5e8 style O2 fill:#e8f5e8 style O3 fill:#e8f5e8
Types of Reactive Components
Reactive Sources (Inputs):
- User interface inputs (
input$*
) - Reactive values (
reactiveVal()
,reactiveValues()
) - File system changes
- Database updates
Reactive Conductors (Processing):
reactive()
expressions for data processingeventReactive()
for event-driven computation- Custom reactive functions
Reactive Endpoints (Outputs):
render*()
functions for displayobserve()
functions for side effects- Download handlers
- Database writes
Reactive Execution Model
# Example demonstrating reactive flow
<- function(input, output, session) {
server
# Reactive source: user input
# Automatically updates when user changes selection
# Reactive conductor: data processing
<- reactive({
processed_data cat("Processing data...\n") # This only prints when recalculated
<- switch(input$dataset,
raw_data "mtcars" = mtcars,
"iris" = iris)
# Expensive data processing
transform_data(raw_data)
})
# Reactive endpoint: output generation
$plot <- renderPlot({
outputcat("Rendering plot...\n") # Only prints when plot updates
<- processed_data() # Uses cached result if available
data create_visualization(data, input$plot_type)
})
# Another endpoint using same processed data
$summary <- renderText({
output<- processed_data() # Reuses cached computation
data generate_summary(data)
}) }
Visualize and control reactive execution in real-time:
- Trigger reactive chains - See how input changes cascade through the system
- Control execution timing - Add delays to observe reactive dependencies
- Monitor execution counts - Track how often each reactive expression runs
- Test validation patterns - Experience req() and validate() in action
- Experiment with isolate() - Break reactive dependencies strategically
Key Learning: Reactive programming eliminates manual update management by automatically tracking dependencies and efficiently updating only what needs to change.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 1000
#| editorHeight: 300
library(shiny)
library(bslib)
library(bsicons)
ui <- page_navbar(
title = "Reactive Flow Simulator",
theme = bs_theme(version = 5, bootswatch = "cosmo"),
nav_panel(
"Flow Control",
layout_columns(
col_widths = c(4, 8),
card(
card_header(
"Reactive Inputs",
bs_icon("input-cursor")
),
sliderInput("data_size", "Dataset Size:",
min = 10, max = 1000, value = 100, step = 10),
selectInput("distribution", "Distribution:",
choices = c("Normal" = "norm",
"Uniform" = "unif",
"Exponential" = "exp")),
numericInput("seed", "Random Seed:",
value = 123, min = 1, max = 999),
hr(),
h6("Reactive Control"),
checkboxInput("use_req", "Use req() validation", TRUE),
checkboxInput("use_validate", "Use validate() messages", FALSE),
checkboxInput("use_isolate", "Isolate seed from plot", FALSE),
sliderInput("artificial_delay", "Artificial Delay (ms):",
min = 0, max = 1000, value = 0, step = 100),
hr(),
actionButton("reset_counters", "Reset Execution Counters",
class = "btn-outline-secondary w-100")
),
card(
card_header(
"Reactive Chain Visualization",
bs_icon("diagram-2")
),
navset_card_tab(
nav_panel(
"Live Output",
br(),
plotOutput("data_plot", height = "100px"),
hr(),
verbatimTextOutput("data_summary")
),
nav_panel(
"Execution Flow",
br(),
div(
style = "font-family: monospace; background: #f8f9fa; padding: 15px; border-radius: 5px;",
verbatimTextOutput("execution_flow")
),
br(),
div(
class = "row",
div(
class = "col-md-6",
h6("Reactive Expression Stats:"),
verbatimTextOutput("reactive_stats")
),
div(
class = "col-md-6",
h6("Performance Metrics:"),
verbatimTextOutput("performance_metrics")
)
)
),
nav_panel(
"Dependency Graph",
br(),
plotOutput("dependency_graph", height = "400px")
)
)
)
)
),
nav_panel(
"Advanced Patterns",
layout_columns(
col_widths = c(6, 6),
card(
card_header(
"EventReactive Pattern",
bs_icon("lightning")
),
textInput("event_input", "Enter text:", value = "Hello Shiny!"),
actionButton("trigger_event", "Trigger Processing",
class = "btn-primary"),
br(), br(),
div(
class = "alert alert-info",
h6("EventReactive Output:"),
verbatimTextOutput("event_output")
),
verbatimTextOutput("event_stats")
),
card(
card_header(
"Observer vs Reactive",
bs_icon("eye")
),
sliderInput("observer_input", "Test Value:",
min = 1, max = 100, value = 50),
br(),
h6("Observer Log (Side Effects):"),
verbatimTextOutput("observer_log"),
br(),
h6("Reactive Expression Result:"),
verbatimTextOutput("reactive_result")
)
)
)
)
server <- function(input, output, session) {
# Simple reactive values for counters - NO dependencies
execution_count <- reactiveVal(0)
event_count <- reactiveVal(0)
observer_count <- reactiveVal(0)
start_time <- reactiveVal(Sys.time())
# Reset counters
observeEvent(input$reset_counters, {
execution_count(0)
event_count(0)
observer_count(0)
start_time(Sys.time())
})
# PURE reactive expression - NO side effects
reactive_data <- reactive({
# Basic validation
req(input$data_size > 0)
req(input$distribution %in% c("norm", "unif", "exp"))
# Validation with req()
if (input$use_req) {
req(input$data_size >= 10, "Size must be >= 10")
req(input$seed, "Seed required")
}
# Validation with validate()
if (input$use_validate) {
validate(
need(input$data_size >= 10, "Dataset size must be at least 10"),
need(input$data_size <= 1000, "Dataset size too large"),
need(input$distribution != "", "Please select a distribution")
)
}
# Artificial delay
if (input$artificial_delay > 0) {
Sys.sleep(input$artificial_delay / 1000)
}
# Handle isolation
seed_val <- if (input$use_isolate) {
isolate(input$seed)
} else {
input$seed
}
set.seed(seed_val)
# Generate data
data_values <- switch(input$distribution,
"norm" = rnorm(input$data_size),
"unif" = runif(input$data_size),
"exp" = rexp(input$data_size),
rnorm(input$data_size)) # fallback
list(
values = data_values,
size = input$data_size,
dist = input$distribution,
seed = seed_val,
timestamp = Sys.time()
)
})
# Track executions - separate observer
observe({
reactive_data() # Create dependency
isolate({
execution_count(execution_count() + 1)
})
})
# Plot output
output$data_plot <- renderPlot({
data_info <- reactive_data()
hist(data_info$values,
breaks = min(30, max(5, data_info$size / 10)),
main = paste("Distribution:", data_info$dist, "| Size:", data_info$size),
xlab = "Value",
col = "steelblue",
border = "white")
})
# Summary output
output$data_summary <- renderPrint({
data_info <- reactive_data()
cat("Summary Statistics:\n")
cat("==================\n")
cat("Distribution:", data_info$dist, "\n")
cat("Sample size:", data_info$size, "\n")
cat("Seed used:", data_info$seed, "\n")
cat("Generated at:", format(data_info$timestamp, "%H:%M:%S"), "\n\n")
print(summary(data_info$values))
})
# Execution flow
output$execution_flow <- renderText({
# Read current values without creating dependencies
current_executions <- execution_count()
paste(
"REACTIVE EXECUTION FLOW",
"========================",
"",
"1. Input Changes Detected:",
paste(" • data_size:", input$data_size),
paste(" • distribution:", input$distribution),
paste(" • seed:", input$seed),
"",
"2. Reactive Expression Status:",
paste(" • Total executions:", current_executions),
paste(" • Validation:", if(input$use_req || input$use_validate) "ENABLED" else "DISABLED"),
paste(" • Isolation:", if(input$use_isolate) "seed ISOLATED" else "all inputs REACTIVE"),
"",
"3. Output Updates:",
" • Plot updates automatically",
" • Summary updates automatically",
" • Shared computation (efficient)",
"",
"4. Dependencies:",
" • Both outputs depend on reactive_data()",
" • Changes cascade automatically",
" • Efficient caching prevents redundant work",
sep = "\n"
)
})
# Reactive statistics
output$reactive_stats <- renderText({
runtime <- as.numeric(difftime(Sys.time(), start_time(), units = "secs"))
paste(
"Execution Counts:",
"================",
paste("Data Reactive:", execution_count()),
paste("Runtime:", round(runtime, 1), "seconds"),
paste("Delay setting:", input$artificial_delay, "ms"),
"",
"Efficiency:",
"• Single computation shared across outputs",
"• Lazy evaluation (only when needed)",
sep = "\n"
)
})
# Performance metrics
output$performance_metrics <- renderText({
paste(
"Reactive Features:",
"=================",
paste("Pattern: EFFICIENT"),
paste("Validation:", if(input$use_req || input$use_validate) "ACTIVE" else "INACTIVE"),
paste("Isolation:", if(input$use_isolate) "ACTIVE" else "INACTIVE"),
"",
"Benefits:",
"• Automatic dependency tracking",
"• Prevents redundant computation",
"• Lazy evaluation",
"• Error handling built-in",
sep = "\n"
)
})
# Dependency graph
output$dependency_graph <- renderPlot({
par(mar = c(2, 2, 3, 2))
plot.new()
plot.window(xlim = c(0, 10), ylim = c(0, 10))
title("Reactive Dependency Graph", cex.main = 1.5, font.main = 2)
# Input nodes
rect(1, 7.5, 3, 8.5, col = "lightblue", border = "navy", lwd = 2)
text(2, 8, "INPUTS", cex = 1.1, font = 2)
text(0.5, 7, "data_size", cex = 0.8)
text(2, 7, "distribution", cex = 0.8)
seed_color <- if(input$use_isolate) "red" else "black"
text(3.5, 7, if(input$use_isolate) "seed (isolated)" else "seed",
cex = 0.8, col = seed_color)
# Reactive expression
rect(4, 4.5, 6, 5.5, col = "lightgreen", border = "darkgreen", lwd = 2)
text(5, 5, "reactive_data()", cex = 1.1, font = 2)
text(5, 4.2, paste("Runs:", execution_count()), cex = 0.8)
# Outputs
rect(7.5, 6.5, 9.5, 7.5, col = "lightyellow", border = "orange", lwd = 2)
text(8.5, 7, "Plot", cex = 1, font = 2)
rect(7.5, 2.5, 9.5, 3.5, col = "lightyellow", border = "orange", lwd = 2)
text(8.5, 3, "Summary", cex = 1, font = 2)
# Arrows
if (!input$use_isolate) {
arrows(3, 7.8, 4, 5.5, lwd = 2, col = "blue")
} else {
arrows(2.5, 7.8, 4, 5.5, lwd = 2, col = "blue")
}
arrows(6, 5.2, 7.5, 6.8, lwd = 2, col = "darkgreen")
arrows(6, 4.8, 7.5, 3.2, lwd = 2, col = "darkgreen")
# Legend
text(1, 1.5, "Blue: Input → Reactive", col = "blue", cex = 0.9)
text(1, 1, "Green: Reactive → Output", col = "darkgreen", cex = 0.9)
if(input$use_isolate) {
text(1, 0.5, "Red: Isolated input", col = "red", cex = 0.9)
}
})
# EventReactive pattern - CLEAN
event_data <- eventReactive(input$trigger_event, {
req(input$event_input)
text_val <- input$event_input
list(
text = text_val,
length = nchar(text_val),
words = length(strsplit(text_val, "\\s+")[[1]]),
timestamp = Sys.time()
)
})
# Track event executions
observeEvent(input$trigger_event, {
event_count(event_count() + 1)
})
# Event output
output$event_output <- renderText({
if (input$trigger_event == 0) {
return("Click 'Trigger Processing' to see eventReactive in action")
}
data <- event_data()
paste(
"Processed text:", data$text,
"\nCharacters:", data$length,
"\nWords:", data$words,
"\nProcessed at:", format(data$timestamp, "%H:%M:%S")
)
})
# Event statistics
output$event_stats <- renderText({
paste(
"EventReactive Stats:",
"===================",
paste("Button clicks:", input$trigger_event),
paste("Processing runs:", event_count()),
"",
"Note: eventReactive only executes when",
"the trigger event occurs, not when the",
"input text changes.",
sep = "\n"
)
})
# Observer demonstration - clean tracking
observe({
input$observer_input # Create dependency
isolate({
observer_count(observer_count() + 1)
})
})
# Reactive expression for observer demo
observer_reactive <- reactive({
req(input$observer_input)
input$observer_input * 2
})
# Observer log
output$observer_log <- renderText({
paste(
"Observer executions:", observer_count(),
"\nLast input value:", input$observer_input,
"\nCurrent time:", format(Sys.time(), "%H:%M:%S"),
"\n\nObservers execute immediately when",
"dependencies change (for side effects)."
)
})
# Reactive result
output$reactive_result <- renderText({
result <- observer_reactive()
paste(
"Input value:", input$observer_input,
"\nReactive result:", result,
"\n\nReactive expressions are lazy:",
"only execute when result is needed."
)
})
}
shinyApp(ui, server)
Session Management and Isolation
Each user connection to a Shiny application creates an isolated session with its own environment and state management.
Session Lifecycle
Session Creation:
- New user connects to application
- Isolated R environment is created
- UI is rendered and sent to browser
- Server function is executed with session parameters
Session Maintenance:
- Input/output state is maintained per session
- Reactive dependencies are tracked independently
- Memory and resources are managed per session
Session Termination:
- User closes browser or navigates away
- Session resources are cleaned up
- Temporary files and connections are closed
Session Object Properties
<- function(input, output, session) {
server
# Session information
observe({
cat("Session ID:", session$token, "\n")
cat("User agent:", session$clientData$user_agent, "\n")
cat("Screen resolution:",
$clientData$pixelratio, "\n")
session
})
# Session-specific reactive values
<- reactiveValues(
user_data login_time = Sys.time(),
page_views = 0,
selections = list()
)
# Update session state
observeEvent(input$any_input, {
$page_views <- user_data$page_views + 1
user_data
}) }
Application File Structure and Organization
Understanding how to organize Shiny applications is crucial for maintainability and scalability.
Best for:
- Simple applications with limited functionality
- Prototypes and proof-of-concepts
- Learning and educational examples
Structure:
# app.R
library(shiny)
# UI definition
<- fluidPage(
ui # UI components
)
# Server logic
<- function(input, output, session) {
server # Server logic
}
# Application execution
shinyApp(ui = ui, server = server)
Best for:
- Complex applications with extensive functionality
- Production applications requiring maintainability
- Team development projects
File Structure:
my-shiny-app/
├── ui.R # UI definition
├── server.R # Server logic
├── global.R # Global variables and functions
├── www/ # Static web assets
│ ├── custom.css
│ ├── custom.js
│ └── images/
├── R/ # Custom R functions
│ ├── data_processing.R
│ ├── plotting.R
│ └── utilities.R
├── data/ # Data files
│ ├── raw/
│ └── processed/
└── modules/ # Shiny modules
├── data_input_module.R
└── visualization_module.R
Code Organization Best Practices
# global.R - Executed once when app starts
library(shiny)
library(dplyr)
library(ggplot2)
# Load data (shared across all sessions)
<- read.csv("data/dataset.csv")
global_data
# Define constants
<- "My Dashboard"
APP_TITLE <- "bootstrap"
DEFAULT_THEME
# Utility functions
source("R/utilities.R")
source("R/plotting.R")
# ui.R - User interface definition
source("modules/sidebar_module.R")
source("modules/main_panel_module.R")
<- navbarPage(
ui title = APP_TITLE,
tabPanel("Dashboard",
fluidRow(
column(4, sidebarModuleUI("sidebar")),
column(8, mainPanelModuleUI("main"))
)
),
tabPanel("About",
includeHTML("www/about.html")
) )
# server.R - Server logic
source("modules/data_processing.R")
source("modules/visualization.R")
<- function(input, output, session) {
server
# Call modules
<- callModule(sidebarModule, "sidebar")
sidebar_data callModule(mainPanelModule, "main", sidebar_data)
# Global observers
observeEvent(input$help_button, {
showModal(modalDialog(
title = "Help",
includeHTML("www/help.html")
))
}) }
Modular Architecture with Shiny Modules
Shiny modules enable building reusable, encapsulated components that can be combined to create complex applications.
Module Structure and Benefits
Benefits of Modular Design:
- Code reusability across multiple applications
- Namespace isolation preventing ID conflicts
- Team development with clear component boundaries
- Testing isolation for individual components
- Maintenance efficiency with focused, single-purpose modules
Creating Shiny Modules
# modules/data_selector_module.R
<- function(id) {
dataSelectorUI <- NS(id)
ns
tagList(
selectInput(ns("dataset"), "Choose Dataset:",
choices = c("mtcars", "iris", "airquality")),
selectInput(ns("variable"), "Choose Variable:",
choices = NULL),
actionButton(ns("update"), "Update Analysis",
class = "btn-primary")
) }
<- function(id) {
dataSelectorServer moduleServer(id, function(input, output, session) {
# Reactive data based on selection
<- reactive({
selected_data switch(input$dataset,
"mtcars" = mtcars,
"iris" = iris,
"airquality" = airquality)
})
# Update variable choices based on dataset
observe({
<- selected_data()
data <- names(data)[sapply(data, is.numeric)]
numeric_vars
updateSelectInput(session, "variable",
choices = numeric_vars,
selected = numeric_vars[1])
})
# Return reactive values for parent to use
return(reactive({
req(input$update, cancelOutput = TRUE)
list(
data = selected_data(),
variable = input$variable,
dataset_name = input$dataset
)
}))
}) }
# In ui.R
<- fluidPage(
ui titlePanel("Modular Shiny Application"),
sidebarLayout(
sidebarPanel(
dataSelectorUI("data_selector")
),mainPanel(
plotOutput("main_plot"),
verbatimTextOutput("data_summary")
)
)
)
# In server.R
<- function(input, output, session) {
server
# Call module and get returned values
<- dataSelectorServer("data_selector")
selected_data
# Use module outputs in main application
$main_plot <- renderPlot({
outputreq(selected_data())
<- selected_data()
data_info hist(data_info$data[[data_info$variable]],
main = paste("Distribution of", data_info$variable,
"in", data_info$dataset_name))
})
$data_summary <- renderPrint({
outputreq(selected_data())
<- selected_data()
data_info summary(data_info$data[[data_info$variable]])
}) }
Edit the code below and click Run to see your changes instantly. Experiment with different parameters, styling options, or add new features to understand how the script works.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 600
#| editorHeight: 300
## file: app.R
# Main Application File for Shiny App with Modules
# Loading Required Libraries
library(shiny)
# Loading Modules
source("modules/data_selector_module_ui.R")
source("modules/data_selector_module_server.R")
# Use Modules in Main Application
# In ui.R
ui <- fluidPage(
titlePanel("Modular Shiny Application"),
sidebarLayout(
sidebarPanel(
dataSelectorUI("data_selector")
),
mainPanel(
plotOutput("main_plot"),
verbatimTextOutput("data_summary")
)
)
)
# In server.R
server <- function(input, output, session) {
# Call module and get returned values
selected_data <- dataSelectorServer("data_selector")
# Use module outputs in main application
output$main_plot <- renderPlot({
req(selected_data())
data_info <- selected_data()
hist(data_info$data[[data_info$variable]],
main = paste("Distribution of", data_info$variable,
"in", data_info$dataset_name))
})
output$data_summary <- renderPrint({
req(selected_data())
data_info <- selected_data()
summary(data_info$data[[data_info$variable]])
})
}
# Run the application
shinyApp(ui = ui, server = server)
## file: modules/data_selector_module_ui.R
# Module UI for selecting datasets and variables
dataSelectorUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("dataset"), "Choose Dataset:",
choices = c("mtcars", "iris", "airquality"),
selectize = FALSE),
selectInput(ns("variable"), "Choose Variable:",
choices = NULL, selectize = FALSE),
actionButton(ns("update"), "Update Analysis",
class = "btn-primary")
)
}
## file: modules/data_selector_module_server.R
# Module server function for data selection
dataSelectorServer <- function(id) {
moduleServer(id, function(input, output, session) {
# Reactive data based on selection
selected_data <- reactive({
switch(input$dataset,
"mtcars" = mtcars,
"iris" = iris,
"airquality" = airquality)
})
# Update variable choices based on dataset
observe({
data <- selected_data()
numeric_vars <- names(data)[sapply(data, is.numeric)]
updateSelectInput(session, "variable",
choices = numeric_vars,
selected = numeric_vars[1])
})
# Return reactive values for parent to use
return(reactive({
req(input$update, cancelOutput = TRUE)
list(
data = selected_data(),
variable = input$variable,
dataset_name = input$dataset
)
}))
})
}
Module Communication Workshop
Practice building and connecting reusable Shiny modules:
- Create module instances - Build data selector and visualization modules
- Connect module communication - Pass data between independent modules
- Test namespace isolation - Verify modules don’t interfere with each other
- Modify module parameters - Experience module reusability and configuration
- Build module hierarchies - Combine modules into larger functional units
Key Learning: Modules provide the building blocks for scalable applications by encapsulating functionality and enabling clean interfaces between components.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 1200
#| editorHeight: 300
library(shiny)
library(bslib)
library(bsicons)
# Data Selector Module
dataSelectorUI <- function(id, label = "Data Selector") {
ns <- NS(id)
card(
card_header(
label,
bs_icon("database")
),
selectInput(ns("dataset"), "Choose Dataset:",
choices = c("Motor Trend Cars" = "mtcars",
"Iris Flowers" = "iris",
"Air Quality" = "airquality",
"Built-in CO2" = "co2_data")),
conditionalPanel(
condition = paste0("input['", ns("dataset"), "'] != ''"),
selectInput(ns("x_variable"), "X Variable:", choices = NULL),
selectInput(ns("y_variable"), "Y Variable:", choices = NULL)
),
actionButton(ns("update"), "Update Selection",
class = "btn-primary w-100"),
hr(),
div(
class = "small text-muted",
textOutput(ns("module_info"))
)
)
}
dataSelectorServer <- function(id, config = list()) {
moduleServer(id, function(input, output, session) {
# Available datasets
available_data <- list(
"mtcars" = mtcars,
"iris" = iris,
"airquality" = airquality,
"co2_data" = data.frame(
year = 1959:1997,
co2 = as.numeric(co2),
trend = seq_along(co2)
)
)
# Get selected dataset
selected_dataset <- reactive({
req(input$dataset)
available_data[[input$dataset]]
})
# Update variable choices when dataset changes
observe({
data <- selected_dataset()
req(data)
numeric_vars <- names(data)[sapply(data, is.numeric)]
updateSelectInput(session, "x_variable",
choices = numeric_vars,
selected = numeric_vars[1])
updateSelectInput(session, "y_variable",
choices = numeric_vars,
selected = numeric_vars[min(2, length(numeric_vars))])
})
# Module information with better formatting
output$module_info <- renderText({
paste("Module:", id, "| Status: Active")
})
# Return reactive data when update is clicked
return(eventReactive(input$update, {
req(input$dataset, input$x_variable, input$y_variable)
data <- selected_dataset()
list(
data = data,
x_var = input$x_variable,
y_var = input$y_variable,
dataset_name = input$dataset,
n_rows = nrow(data),
n_cols = ncol(data),
module_id = id
)
}, ignoreNULL = FALSE))
})
}
# Visualization Module
visualizationUI <- function(id, title = "Visualization") {
ns <- NS(id)
card(
card_header(
title,
bs_icon("graph-up")
),
div(
class = "row",
div(
class = "col-md-6",
selectInput(ns("plot_type"), "Plot Type:",
choices = c("Scatter Plot" = "scatter",
"Line Plot" = "line",
"Box Plot" = "box",
"Histogram" = "hist"))
),
div(
class = "col-md-6",
selectInput(ns("color_scheme"), "Color Scheme:",
choices = c("Default" = "default",
"Viridis" = "viridis",
"Red-Blue" = "redblue"))
)
),
conditionalPanel(
condition = paste0("input['", ns("plot_type"), "'] == 'scatter'"),
sliderInput(ns("point_size"), "Point Size:",
min = 0.5, max = 3, value = 1.5, step = 0.25)
),
plotOutput(ns("plot"), height = "220px"),
div(style = "margin-top: 10px; padding: 8px 12px; background-color: #f8f9fa; border-radius: 4px; font-size: 0.85em; line-height: 1.2;",
verbatimTextOutput(ns("data_info"))
)
)
}
visualizationServer <- function(id, data_source) {
moduleServer(id, function(input, output, session) {
# Add a reactive to safely check data availability
safe_data <- reactive({
tryCatch({
data_result <- data_source()
if (is.null(data_result) || length(data_result) == 0) {
return(NULL)
}
return(data_result)
}, error = function(e) {
return(NULL)
})
})
output$plot <- renderPlot({
# Check if we have valid data
data_info <- safe_data()
if (is.null(data_info)) {
# Create a placeholder plot with fixed dimensions
par(mar = c(4, 4, 2, 2))
plot(1, 1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
xlab = "", ylab = "", main = "Waiting for data...",
axes = FALSE)
text(0.5, 0.5, "Click 'Update Selection' to load data",
cex = 1.2, col = "gray50")
return()
}
# Validate required fields
req(data_info$data, data_info$x_var, data_info$y_var)
data <- data_info$data
# Safely extract variables with validation
if (!data_info$x_var %in% names(data) || !data_info$y_var %in% names(data)) {
par(mar = c(4, 4, 2, 2))
plot(1, 1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
xlab = "", ylab = "", main = "Variable Error",
axes = FALSE)
text(0.5, 0.5, "Selected variables not found in dataset",
cex = 1.2, col = "red")
return()
}
x_vals <- data[[data_info$x_var]]
y_vals <- data[[data_info$y_var]]
# Validate that we have actual data
if (length(x_vals) == 0 || length(y_vals) == 0 ||
all(is.na(x_vals)) || all(is.na(y_vals))) {
par(mar = c(4, 4, 2, 2))
plot(1, 1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
xlab = "", ylab = "", main = "No Data",
axes = FALSE)
text(0.5, 0.5, "No valid data to plot",
cex = 1.2, col = "red")
return()
}
# Ensure we have valid plot inputs
req(input$plot_type, input$color_scheme)
# Color scheme
base_color <- switch(input$color_scheme,
"default" = "steelblue",
"viridis" = "#440154",
"redblue" = "darkred",
"steelblue") # fallback
# Set proper margins
par(mar = c(4, 4, 3, 2))
# Create plot based on type with comprehensive error handling
tryCatch({
switch(input$plot_type,
"scatter" = {
point_size <- if (is.null(input$point_size)) 1.5 else input$point_size
plot(x_vals, y_vals,
main = paste("Scatter:", data_info$x_var, "vs", data_info$y_var),
xlab = data_info$x_var,
ylab = data_info$y_var,
col = base_color,
pch = 16,
cex = point_size)
},
"line" = {
# Sort by x values for line plot
if (length(x_vals) > 1) {
sorted_indices <- order(x_vals, na.last = NA)
plot(x_vals[sorted_indices], y_vals[sorted_indices],
type = "l",
main = paste("Line:", data_info$x_var, "vs", data_info$y_var),
xlab = data_info$x_var,
ylab = data_info$y_var,
col = base_color,
lwd = 2)
} else {
plot(x_vals, y_vals, pch = 16, col = base_color,
main = paste("Single Point:", data_info$x_var, "vs", data_info$y_var),
xlab = data_info$x_var, ylab = data_info$y_var)
}
},
"box" = {
unique_x <- unique(x_vals[!is.na(x_vals)])
if (length(unique_x) > 1 && length(unique_x) < 10) {
boxplot(y_vals ~ x_vals,
main = paste("Box Plot:", data_info$y_var, "by", data_info$x_var),
xlab = data_info$x_var,
ylab = data_info$y_var,
col = base_color)
} else {
boxplot(y_vals,
main = paste("Box Plot:", data_info$y_var),
ylab = data_info$y_var,
col = base_color)
}
},
"hist" = {
# Ensure we have numeric data for histogram
if (!is.numeric(y_vals)) {
plot(1, 1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
xlab = "", ylab = "", main = "Data Type Error",
axes = FALSE)
text(0.5, 0.5, "Histogram requires numeric data",
cex = 1.2, col = "red")
return()
}
valid_y <- y_vals[!is.na(y_vals)]
if (length(valid_y) < 2) {
plot(valid_y, rep(1, length(valid_y)), pch = 16, col = base_color,
main = paste("Single Value:", data_info$y_var),
xlab = data_info$y_var, ylab = "Count")
} else {
breaks_val <- max(5, min(30, length(valid_y)/5))
hist(valid_y,
main = paste("Histogram:", data_info$y_var),
xlab = data_info$y_var,
col = base_color,
border = "white",
breaks = breaks_val)
}
},
{
# Default case
plot(1, 1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
xlab = "", ylab = "", main = "Unknown Plot Type",
axes = FALSE)
text(0.5, 0.5, "Unknown plot type selected",
cex = 1.2, col = "red")
})
}, error = function(e) {
# If all else fails, create a simple error plot
par(mar = c(4, 4, 2, 2))
plot(1, 1, type = "n", xlim = c(0, 1), ylim = c(0, 1),
xlab = "", ylab = "", main = "Plot Error",
axes = FALSE)
text(0.5, 0.5, paste("Error:", substr(e$message, 1, 50)),
cex = 1, col = "red")
})
}, height = 220) # Slightly larger plot
output$data_info <- renderText({
# Safe data info rendering with compact formatting
data_info <- safe_data()
if (is.null(data_info)) {
return("No data selected")
}
# Validate required fields
if (is.null(data_info$dataset_name) || is.null(data_info$x_var) ||
is.null(data_info$y_var) || is.null(data_info$module_id)) {
return("Incomplete data")
}
paste(
paste("Dataset:", data_info$dataset_name),
paste("Size:", data_info$n_rows, "×", data_info$n_cols),
paste("Variables:", data_info$x_var, "vs", data_info$y_var),
sep = " | "
)
})
})
}
# Summary Module
summaryUI <- function(id) {
ns <- NS(id)
card(
card_header(
"Data Summary",
bs_icon("clipboard-data")
),
tabsetPanel(
tabPanel("Statistics", verbatimTextOutput(ns("stats"))),
tabPanel("Structure", verbatimTextOutput(ns("structure"))),
tabPanel("Correlation", verbatimTextOutput(ns("correlation")))
)
)
}
summaryServer <- function(id, data_source) {
moduleServer(id, function(input, output, session) {
output$stats <- renderPrint({
data_info <- data_source()
req(data_info)
cat("Dataset:", data_info$dataset_name, "\n\n")
x_vals <- data_info$data[[data_info$x_var]]
y_vals <- data_info$data[[data_info$y_var]]
cat("X Variable (", data_info$x_var, "):\n", sep = "")
print(summary(x_vals))
cat("\nY Variable (", data_info$y_var, "):\n", sep = "")
print(summary(y_vals))
})
output$structure <- renderPrint({
data_info <- data_source()
req(data_info)
cat("Dataset Structure for:", data_info$dataset_name, "\n")
cat("=====================================\n\n")
str(data_info$data)
})
output$correlation <- renderPrint({
data_info <- data_source()
req(data_info)
data <- data_info$data
numeric_vars <- sapply(data, is.numeric)
if (sum(numeric_vars) >= 2) {
cat("Correlation Matrix (Numeric Variables):\n")
cat("======================================\n\n")
numeric_data <- data[numeric_vars]
print(round(cor(numeric_data, use = "complete.obs"), 3))
} else {
cat("Not enough numeric variables for correlation analysis.")
}
})
})
}
# Main Application
ui <- page_navbar(
title = "Module Communication Workshop",
theme = bs_theme(version = 5, bootswatch = "cosmo"),
nav_panel(
"Basic Communication",
layout_columns(
col_widths = c(4, 8),
dataSelectorUI("selector1", "Primary Data Selector"),
card(
card_header(
"Module Outputs",
bs_icon("arrows-angle-expand")
),
tabsetPanel(
tabPanel("Visualization",
visualizationUI("viz1", "Primary Visualization")),
tabPanel("Summary",
summaryUI("summary1"))
)
)
)
),
nav_panel(
"Multiple Modules",
# Main content: Two columns
fluidRow(
# Left column: Data selectors
column(4,
card(
card_header(
"Data Selectors",
bs_icon("collection")
),
style = "height: 650px; overflow-y: auto;",
div(style = "margin-bottom: 30px;",
dataSelectorUI("selector2", "Dataset A")
),
div(style = "margin-top: 20px;",
dataSelectorUI("selector3", "Dataset B")
)
)
),
# Right column: Visualizations (stacked)
column(8,
# Visualization A (top)
div(style = "margin-bottom: 25px;",
card(
style = "height: 300px;",
visualizationUI("viz2", "Visualization A")
)
),
# Visualization B (bottom)
card(
style = "height: 300px;",
visualizationUI("viz3", "Visualization B")
)
)
),
# Bottom section: Communication status (full width)
fluidRow(
column(12,
card(
card_header(
"Module Communication Status",
bs_icon("diagram-3")
),
style = "margin-top: 30px;",
verbatimTextOutput("communication_status")
)
)
)
),
nav_panel(
"Module Inspector",
layout_columns(
col_widths = c(6, 6),
card(
card_header(
"Module Namespace Analysis",
bs_icon("search")
),
h6("Active Module Instances:"),
verbatimTextOutput("module_instances"),
hr(),
h6("Namespace Verification:"),
verbatimTextOutput("namespace_check")
),
card(
card_header(
"Module Performance",
bs_icon("speedometer")
),
verbatimTextOutput("module_performance")
)
)
)
)
server <- function(input, output, session) {
# Module instances with communication tracking
data1 <- dataSelectorServer("selector1")
data2 <- dataSelectorServer("selector2")
data3 <- dataSelectorServer("selector3")
# Visualization modules
visualizationServer("viz1", data1)
visualizationServer("viz2", data2)
visualizationServer("viz3", data3)
# Summary modules
summaryServer("summary1", data1)
# Communication status tracking
output$communication_status <- renderText({
# Safe checking for data availability
data2_status <- tryCatch({
data2_result <- data2()
!is.null(data2_result) && length(data2_result) > 0
}, error = function(e) FALSE)
data3_status <- tryCatch({
data3_result <- data3()
!is.null(data3_result) && length(data3_result) > 0
}, error = function(e) FALSE)
status_a <- if (data2_status) "ACTIVE" else "WAITING"
status_b <- if (data3_status) "ACTIVE" else "WAITING"
paste(
"Module Communication Status:",
"============================",
"",
"selector2 → viz2:", status_a,
"selector3 → viz3:", status_b,
"",
"Module Isolation Check:",
"- Each module has independent state",
"- Namespace prevents ID conflicts",
"- Modules can be reused safely",
"",
"Data Flow:",
"selector2() →", if(status_a == "ACTIVE") "data passed" else "no data",
"selector3() →", if(status_b == "ACTIVE") "data passed" else "no data",
sep = "\n"
)
})
# Module inspection
output$module_instances <- renderText({
paste(
"Registered Module Instances:",
"============================",
"",
"Data Selectors:",
" • selector1 (Primary)",
" • selector2 (Dataset A)",
" • selector3 (Dataset B)",
"",
"Visualization Modules:",
" • viz1 (Primary)",
" • viz2 (Visualization A)",
" • viz3 (Visualization B)",
"",
"Summary Modules:",
" • summary1 (Primary)",
"",
"Total Active Modules: 6",
sep = "\n"
)
})
output$namespace_check <- renderText({
paste(
"Namespace Isolation Test:",
"========================",
"",
"Input IDs are properly namespaced:",
" • selector1-dataset vs selector2-dataset",
" • viz1-plot_type vs viz2-plot_type",
" • No ID conflicts detected",
"",
"Module Communication:",
" • Modules communicate through reactive returns",
" • No global variables required",
" • Clean separation of concerns",
"",
"Reusability:",
" • Same module used multiple times",
" • Each instance independent",
" • Configurable parameters",
sep = "\n"
)
})
output$module_performance <- renderText({
# Safe performance tracking
data1_status <- tryCatch({
data1_result <- data1()
!is.null(data1_result) && length(data1_result) > 0
}, error = function(e) FALSE)
data2_status <- tryCatch({
data2_result <- data2()
!is.null(data2_result) && length(data2_result) > 0
}, error = function(e) FALSE)
data3_status <- tryCatch({
data3_result <- data3()
!is.null(data3_result) && length(data3_result) > 0
}, error = function(e) FALSE)
active_connections <- sum(data1_status, data2_status, data3_status)
paste(
"Module Performance Metrics:",
"==========================",
"",
paste("Active data connections:", active_connections, "/ 3"),
paste("Memory efficiency: OPTIMIZED"),
paste("Reactive caching: ENABLED"),
"",
"Benefits of Modular Design:",
" • Code reusability: HIGH",
" • Maintenance: SIMPLIFIED",
" • Testing: ISOLATED",
" • Debugging: FOCUSED",
"",
"Best Practices Implemented:",
" ✓ Single responsibility per module",
" ✓ Clear input/output interfaces",
" ✓ Namespace isolation",
" ✓ Reactive communication patterns",
sep = "\n"
)
})
}
shinyApp(ui, server)
Data Flow and Communication Patterns
Understanding how data flows through Shiny applications helps optimize performance and debug issues.
Input-to-Output Data Flow
sequenceDiagram participant User participant Browser participant Shiny participant Server participant R User->>Browser: Interacts with input Browser->>Shiny: Sends input value Shiny->>Server: Updates input$ object Server->>R: Executes reactive code R->>Server: Returns computed result Server->>Shiny: Sends output result Shiny->>Browser: Updates DOM element Browser->>User: Displays updated content
Communication Between Components
Parent-Child Communication:
# Parent passes data to child module
<- reactive({
parent_data process_main_data(input$main_selection)
})
<- childModuleServer("child", parent_data)
child_result
# Child returns processed data to parent
$parent_output <- renderPlot({
output<- child_result()
result create_plot(result)
})
Sibling Module Communication:
# Using shared reactive values
<- reactiveValues()
shared_state
# Module 1 updates shared state
moduleServer("module1", function(input, output, session) {
observeEvent(input$update, {
$data <- process_data(input$selection)
shared_state
})
})
# Module 2 reacts to shared state changes
moduleServer("module2", function(input, output, session) {
$plot <- renderPlot({
outputreq(shared_state$data)
create_visualization(shared_state$data)
}) })
Performance Considerations in Architecture
Understanding architectural performance implications helps build efficient applications.
Reactive System Optimization
Efficient Reactive Design:
# GOOD: Efficient reactive chain
<- function(input, output, session) {
server
# Single reactive for expensive data processing
<- reactive({
processed_data expensive_computation(input$dataset)
})
# Multiple outputs use cached result
$plot1 <- renderPlot({
outputcreate_plot1(processed_data())
})
$plot2 <- renderPlot({
outputcreate_plot2(processed_data())
})
$table <- renderTable({
outputprocessed_data()
}) }
Avoiding Performance Pitfalls:
# BAD: Redundant computations
<- function(input, output, session) {
server
# Each output repeats expensive computation
$plot1 <- renderPlot({
output<- expensive_computation(input$dataset) # Computed 3 times!
data create_plot1(data)
})
$plot2 <- renderPlot({
output<- expensive_computation(input$dataset) # Redundant
data create_plot2(data)
})
$table <- renderTable({
outputexpensive_computation(input$dataset) # Redundant
}) }
Memory Management
Session-Level Memory Management:
<- function(input, output, session) {
server
# Clean up resources when session ends
$onSessionEnded(function() {
session# Close database connections
if (exists("db_connection")) {
::dbDisconnect(db_connection)
DBI
}
# Remove large objects
if (exists("large_dataset")) {
rm(large_dataset)
}
# Clean temporary files
<- list.files(tempdir(), pattern = "app_temp_")
temp_files file.remove(file.path(tempdir(), temp_files))
})
# Use req() to prevent unnecessary computations
$expensive_plot <- renderPlot({
outputreq(input$show_plot == TRUE) # Only compute when needed
req(input$dataset != "") # Require valid input
expensive_visualization(input$dataset)
}) }
Common Issues and Solutions
Issue 1: Reactive Dependency Problems
Problem: Outputs not updating when inputs change, or unexpected update cascades.
Solution:
# Diagnose reactive dependencies
<- function(input, output, session) {
server
# Enable reactive logging (development only)
if (interactive()) {
options(shiny.reactlog = TRUE)
}
# Use req() to control execution flow
<- reactive({
filtered_data req(input$dataset) # Wait for valid input
req(input$filter_value)
%>% filter(column == input$filter_value)
data
})
# Use isolate() to break unwanted dependencies
$info_text <- renderText({
output<- nrow(filtered_data())
data_count <- isolate(Sys.time()) # Don't react to time changes
current_time
paste("Data updated at", current_time, "with", data_count, "rows")
}) }
Issue 2: Module Communication Failures
Problem: Modules not communicating properly or namespace conflicts.
Solution:
# Proper module structure with clear communication
<- function(id) {
parentModuleServer moduleServer(id, function(input, output, session) {
# Create shared reactive values
<- reactiveValues(
shared_data selected_dataset = NULL,
processed_data = NULL
)
# Child module 1: Data selection
observe({
<- childModule1Server("child1")
child1_result $selected_dataset <- child1_result()
shared_data
})
# Child module 2: Data processing
observe({
req(shared_data$selected_dataset)
<- childModule2Server("child2", shared_data$selected_dataset)
processed $processed_data <- processed()
shared_data
})
# Return shared data for external use
return(reactive({
$processed_data
shared_data
}))
}) }
Issue 3: Session State Management
Problem: Loss of application state or inconsistent behavior across sessions.
Solution:
<- function(input, output, session) {
server
# Initialize session-specific state
<- reactiveValues(
session_state initialized = FALSE,
user_selections = list(),
app_state = "ready"
)
# Initialize session
observe({
if (!session_state$initialized) {
# Set default values
updateSelectInput(session, "dataset", selected = "mtcars")
$initialized <- TRUE
session_state$app_state <- "initialized"
session_state
}
})
# Track user interactions
observeEvent(input$dataset, {
$user_selections$dataset <- input$dataset
session_state$user_selections$timestamp <- Sys.time()
session_state
})
# Provide session state to modules if needed
return(session_state)
}
Common Questions About Shiny Application Architecture
Use single-file (app.R) when:
- Building simple applications with under 200 lines of code
- Creating prototypes or proof-of-concepts
- Working alone on educational or personal projects
- Need quick deployment without complex organization
Use multi-file structure when:
- Application exceeds 200-300 lines of code
- Working with a team requiring clear code separation
- Building production applications requiring maintainability
- Need to organize complex functionality into logical components
- Plan to create reusable modules or components
The transition point is typically when you find yourself scrolling extensively to find code sections or when multiple people need to work on the same application.
Shiny’s reactive system is declarative and automatic - you describe what should happen, and Shiny manages when and how updates occur. JavaScript frameworks like React require imperative management of state changes and component updates.
Key differences:
- Shiny: Automatic dependency tracking, lazy evaluation, server-side reactivity
- React/Vue: Manual state management, client-side reactivity, explicit update triggers
Shiny’s advantages: Simpler for R users, automatic optimization, integrated with R’s data ecosystem. JavaScript framework advantages: More control over performance, better for complex UI interactions, larger ecosystem of components.
Choose Shiny when data processing and statistical analysis are central to your application. Choose JavaScript frameworks when you need complex UI interactions or client-side performance is critical.
For large datasets, use these architectural strategies:
Data Loading:
# Load data once in global.R, not in reactive contexts
# global.R
<- read_parquet("data/large_file.parquet") # Load once
large_dataset
# server.R
<- reactive({
filtered_data %>%
large_dataset filter(category == input$filter) %>% # Filter early
slice_head(n = 1000) # Limit rows for display
})
Database Integration:
# Use database connections for very large datasets
<- dbPool(RSQLite::SQLite(), dbname = "large_data.db")
pool
<- function(input, output, session) {
server <- reactive({
query_results <- glue::glue("SELECT * FROM table WHERE condition = '{input$filter}' LIMIT 1000")
sql_query dbGetQuery(pool, sql_query)
}) }
Progressive Loading: Load and display data in chunks rather than all at once.
Create focused, single-purpose modules:
# GOOD: Focused module
<- function(id) {
dataVisualizationModule # Only handles visualization, accepts any data format
}
# BAD: Monolithic module
<- function(id) {
dashboardModule # Handles data loading, processing, visualization, and reporting
}
Design principles for reusable modules:
- Single responsibility: Each module should have one clear purpose
- Parameterized inputs: Accept configuration through parameters rather than hard-coding values
- Standard interfaces: Use consistent input/output patterns across modules
- Documentation: Include clear usage examples and parameter descriptions
Organization strategy: Create a personal or organizational module library with categories like “data-input”, “visualization”, “analysis”, and “reporting” modules that can be mixed and matched across applications.
Key security considerations in Shiny architecture:
Input Validation:
<- function(input, output, session) {
server # Validate all inputs before processing
<- reactive({
safe_input req(input$user_input)
validate(
need(nchar(input$user_input) <= 100, "Input too long"),
need(!grepl("[<>\"']", input$user_input), "Invalid characters")
)$user_input
input
}) }
Session Isolation: Shiny automatically isolates user sessions, but be careful with global variables that might leak information between users.
File Upload Security: Validate file types, limit file sizes, and scan uploaded content before processing.
Database Security: Use parameterized queries and connection pooling to prevent SQL injection.
Authentication: For sensitive applications, implement proper user authentication and authorization before allowing access to data or functionality.
Best practices: Never trust user input, validate everything, use HTTPS in production, and follow the principle of least privilege for data access.
Test Your Understanding
In the following code, how many times will the expensive_computation()
function execute when a user changes input$dataset
?
<- function(input, output, session) {
server
$plot1 <- renderPlot({
output<- expensive_computation(input$dataset)
data create_plot1(data)
})
$plot2 <- renderPlot({
output<- expensive_computation(input$dataset)
data create_plot2(data)
})
$summary <- renderText({
output<- expensive_computation(input$dataset)
data summarize_data(data)
}) }
- Once - Shiny automatically caches results
- Three times - once for each output
- It depends on which outputs are visible
- Zero times until outputs are actually rendered
- Consider whether each
renderPlot()
andrenderText()
creates independent reactive contexts - Think about how Shiny handles caching across different reactive endpoints
- Remember that
render*()
functions are reactive endpoints, not reactive expressions
B) Three times - once for each output
The expensive_computation()
function will execute three separate times because:
Problem: Each render*()
function creates its own reactive context. Since expensive_computation()
is called directly within each render function rather than in a shared reactive()
expression, there’s no automatic caching between them.
Better approach:
<- function(input, output, session) {
server
# Create shared reactive expression
<- reactive({
processed_data expensive_computation(input$dataset) # Executes only once
})
$plot1 <- renderPlot({
outputcreate_plot1(processed_data()) # Uses cached result
})
$plot2 <- renderPlot({
outputcreate_plot2(processed_data()) # Uses cached result
})
$summary <- renderText({
outputsummarize_data(processed_data()) # Uses cached result
}) }
Key principle: Use reactive()
expressions for expensive computations that multiple outputs need to share.
You’re building a data analysis application with three main components: data selection, data processing, and visualization. Which modular architecture approach would be most maintainable and reusable?
- Create one large module that handles all three components together
- Create three separate modules that communicate through the main server function
- Put all logic in the main server function without using modules
- Create modules only for the UI components, keep all logic in the main server
- Consider the principles of separation of concerns and single responsibility
- Think about how you would reuse these components in other applications
- Remember that modules should have clear, focused purposes
B) Create three separate modules that communicate through the main server function
This approach provides the best architecture because:
Modular Design Benefits:
# Data Selection Module
<- function(id) {
dataSelectionServer moduleServer(id, function(input, output, session) {
return(reactive({
list(dataset = input$dataset, filters = input$filters)
}))
})
}
# Data Processing Module
<- function(id, raw_data) {
dataProcessingServer moduleServer(id, function(input, output, session) {
<- reactive({
processed req(raw_data())
apply_transformations(raw_data(), input$processing_options)
})return(processed)
})
}
# Visualization Module
<- function(id, processed_data) {
visualizationServer moduleServer(id, function(input, output, session) {
$plot <- renderPlot({
outputreq(processed_data())
create_visualization(processed_data(), input$plot_type)
})
}) }
Why this approach wins:
- Reusability: Each module can be used independently in other applications
- Maintainability: Changes to one component don’t affect others
- Testing: Each module can be tested in isolation
- Team development: Different developers can work on different modules
- Clear interfaces: Well-defined inputs and outputs between components
Your Shiny application processes large datasets and serves multiple concurrent users. The current architecture loads the full dataset for each user session. What’s the best architectural change to improve performance and scalability?
- Increase server RAM and CPU to handle more concurrent processing
- Move data loading to global.R and implement reactive filtering
- Cache results in the browser using JavaScript
- Create separate applications for each type of analysis
- Consider where data loading occurs and how it affects memory usage
- Think about the difference between session-level and application-level resources
- Remember that multiple users sharing resources is more efficient than individual copies
B) Move data loading to global.R and implement reactive filtering
This architectural change provides the most significant performance improvement:
Problem with current architecture:
# BAD: Each session loads full dataset
<- function(input, output, session) {
server <- reactive({
full_dataset read.csv("large_dataset.csv") # Loaded per session!
}) }
Optimized architecture:
# global.R - Load once for all sessions
<- read_parquet("data/large_dataset.parquet") # Loaded once
large_dataset <- c("date", "category", "region") # Pre-index for filtering
index_columns
# server.R - Filter shared data per session
<- function(input, output, session) {
server
# Filter shared dataset (much faster than reloading)
<- reactive({
filtered_data %>%
large_dataset filter(
>= input$date_range[1],
date <= input$date_range[2],
date %in% input$selected_categories
category %>%
) slice_head(n = 10000) # Limit for display performance
})
$analysis <- renderPlot({
outputcreate_analysis(filtered_data())
}) }
Performance benefits:
- Memory efficiency: One copy of data shared across all sessions
- Startup speed: No data loading delay for new users
- Scalability: Server can handle many more concurrent users
- Consistency: All users work with the same data version
Additional optimizations: Consider using databases for very large datasets or implementing data caching strategies.
Conclusion
Understanding Shiny’s application architecture is fundamental to building robust, scalable, and maintainable interactive web applications. The UI-Server separation, reactive programming model, and modular design principles provide a solid foundation for applications ranging from simple dashboards to complex enterprise systems.
The architectural concepts covered in this guide - from basic component relationships to advanced modular design patterns - will serve you throughout your Shiny development journey. As you build more sophisticated applications, these principles become increasingly important for managing complexity, optimizing performance, and ensuring code maintainability.
Mastering Shiny architecture enables you to make informed decisions about application design, troubleshoot issues more effectively, and build applications that scale with your needs and user base.
Next Steps
Based on your understanding of Shiny application architecture, here are the recommended paths for advancing your development skills:
Immediate Next Steps (Complete These First)
- Mastering Reactive Programming in Shiny - Deep dive into reactive expressions, observers, and advanced reactive patterns
- Shiny Layout Systems and Design Patterns - Learn to create professional, responsive layouts that scale with complexity
- Practice Exercise: Refactor an existing single-file application into a modular structure using the principles learned in this guide
Building on Your Foundation (Choose Your Path)
For Advanced Architecture:
For Performance Optimization:
For Production Applications:
Long-term Goals (2-4 Weeks)
- Design and implement a modular application architecture for a real-world project
- Create reusable module library that can be shared across multiple applications
- Optimize an existing application’s performance using architectural best practices
- Contribute to the Shiny community by sharing architectural patterns or creating development tools
Explore More Articles
Here are more articles from the same category to help you dive deeper into the topic.
Reuse
Citation
@online{kassambara2025,
author = {Kassambara, Alboukadel},
title = {Understanding {Shiny} {Application} {Architecture:}
{Complete} {Guide}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/fundamentals/app-structure.html},
langid = {en}
}