flowchart TD subgraph "Reactive Sources" I1[input$dataset] I2[input$filter] I3[input$plot_type] RV[reactiveVal] end subgraph "Reactive Conductors" R1[raw_data] R2[filtered_data] R3[processed_data] end subgraph "Reactive Endpoints" O1[output$plot] O2[output$summary] O3[output$table] OB[observer] end I1 --> R1 R1 --> R2 I2 --> R2 R2 --> R3 R3 --> O1 R3 --> O2 R2 --> O3 I3 --> O1 RV --> OB style I1 fill:#ffebee style I2 fill:#ffebee style I3 fill:#ffebee style RV fill:#ffebee style R1 fill:#f3e5f5 style R2 fill:#f3e5f5 style R3 fill:#f3e5f5 style O1 fill:#e8f5e8 style O2 fill:#e8f5e8 style O3 fill:#e8f5e8 style OB fill:#e8f5e8
Key Takeaways
- Reactive Programming Foundation: Shiny’s reactive system automatically manages dependencies and updates, eliminating manual event handling complexity
- Three Core Types: Reactive sources (inputs), reactive conductors (expressions), and reactive endpoints (outputs and observers) form the complete reactive ecosystem
- Lazy Evaluation Advantage: Reactive expressions only execute when needed and cache results until dependencies change, optimizing performance automatically
- Event-Driven Control: Use
observeEvent()
andeventReactive()
to control when reactions occur, enabling sophisticated user interaction patterns - Advanced Patterns: Master reactive values, invalidation techniques, and conditional reactivity to build professional-grade applications with complex state management
Introduction
Reactive programming is the heart and soul of Shiny applications - it’s what transforms static R code into dynamic, interactive web experiences. Unlike traditional programming where you explicitly control when functions execute, reactive programming creates a declarative system where you describe relationships between inputs and outputs, and Shiny automatically manages the execution flow.
This comprehensive guide will take you from basic reactive concepts to advanced patterns used in production applications. You’ll learn not just how reactive programming works, but when and why to use different reactive patterns, how to optimize performance, and how to avoid common pitfalls that can make applications slow or unpredictable.
Understanding reactive programming deeply will transform how you think about building interactive applications and enable you to create sophisticated, efficient, and maintainable Shiny applications.
Reactive Programming Cheatsheet - All reactive patterns covered in this tutorial condensed into a scannable reference with advanced debugging tips.
Instant Reference • Best Practices • Advanced Patterns
Understanding Reactive Programming Fundamentals
Reactive programming in Shiny is based on a simple but powerful concept: automatic dependency tracking and lazy evaluation. Instead of manually controlling when computations happen, you describe what should happen, and Shiny figures out when to make it happen.
The Reactive Philosophy
Traditional Programming Approach:
# Traditional approach - manual control
<- function() {
user_clicks_button <- load_data()
data <- process_data(data)
processed <- create_plot(processed)
plot display_plot(plot)
}
Reactive Programming Approach:
# Reactive approach - declarative relationships
<- reactive({ load_data() })
data <- reactive({ process_data(data()) })
processed $plot <- renderPlot({ create_plot(processed()) }) output
The reactive approach creates a dependency graph where each component knows what it depends on, and Shiny automatically updates the graph when dependencies change.
The Reactive Dependency Graph
Reactive Dependency Graph Visualizer
Experience Shiny’s reactive magic in real-time and understand the three pillars of reactive programming:
- Watch dependency chains execute - See how input changes cascade through reactive expressions
- Monitor performance in real-time - Understand why reactive design creates efficient applications
- Test different scenarios - Experience simple flows vs complex reactive patterns
- Track execution flow - See exactly when and why each reactive fires
- Experiment with optimization - Discover how caching and dependency management work
Key Learning: This visualization reveals how Shiny’s reactive system creates efficient, responsive applications through intelligent dependency tracking, caching, and selective updates.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 1500
library(shiny)
library(bslib)
library(bsicons)
library(DT)
ui <- fluidPage(
theme = bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
.control-panel {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.control-panel h4 {
margin-bottom: 15px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.dependency-node {
border: 3px solid;
border-radius: 12px;
padding: 15px;
margin: 10px 0;
background: white;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.dependency-node::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
transition: left 0.5s ease;
}
.dependency-node.executing::before {
left: 100%;
}
.source-node {
border-color: #dc3545;
background: linear-gradient(135deg, #fdf2f2 0%, #fff 100%);
}
.conductor-node {
border-color: #6f42c1;
background: linear-gradient(135deg, #f8f6ff 0%, #fff 100%);
}
.endpoint-node {
border-color: #198754;
background: linear-gradient(135deg, #f2f8f4 0%, #fff 100%);
}
.execution-counter {
background: linear-gradient(135deg, #0d6efd, #6610f2);
color: white;
padding: 8px 16px;
border-radius: 25px;
font-size: 1rem;
font-weight: bold;
display: inline-block;
min-width: 40px;
text-align: center;
box-shadow: 0 4px 15px rgba(13, 110, 253, 0.3);
animation: pulse-glow 2s infinite;
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 4px 15px rgba(13, 110, 253, 0.3); }
50% { box-shadow: 0 4px 25px rgba(13, 110, 253, 0.6); }
}
.execution-flow {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
max-height: 300px;
overflow-y: auto;
margin-top: 10px;
}
.flow-entry {
padding: 8px 12px;
margin: 4px 0;
border-radius: 6px;
border-left: 4px solid #6c757d;
background: white;
animation: slideIn 0.3s ease;
}
.flow-source { border-left-color: #dc3545; background: #fef7f7; }
.flow-conductor { border-left-color: #6f42c1; background: #faf9ff; }
.flow-endpoint { border-left-color: #198754; background: #f7fbf8; }
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.dependency-arrow {
position: absolute;
z-index: 10;
pointer-events: none;
}
.arrow-line {
stroke: #6610f2;
stroke-width: 3;
fill: none;
stroke-dasharray: 8,4;
animation: arrow-flow 2s linear infinite;
}
@keyframes arrow-flow {
from { stroke-dashoffset: 0; }
to { stroke-dashoffset: 24; }
}
.performance-metrics {
background: linear-gradient(135deg, #17a2b8, #138496);
color: white;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.metric-card {
background: rgba(255,255,255,0.1);
border-radius: 8px;
padding: 15px;
text-align: center;
backdrop-filter: blur(10px);
}
.metric-value {
font-size: 2rem;
font-weight: bold;
display: block;
}
.scenario-selector {
background: linear-gradient(135deg, #fd7e14, #e55d00);
color: white;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.btn-scenario {
background: rgba(255,255,255,0.2);
border: 2px solid rgba(255,255,255,0.3);
color: white;
margin: 5px;
transition: all 0.3s ease;
}
.btn-scenario:hover {
background: rgba(255,255,255,0.3);
border-color: rgba(255,255,255,0.5);
color: white;
transform: translateY(-2px);
}
.output-panel {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
margin: 10px 0;
}
.real-time-indicator {
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 10px 20px;
border-radius: 25px;
font-weight: bold;
z-index: 1000;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.timing-display {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: #6c757d;
margin-top: 5px;
}
"))
),
# Real-time indicator
div(class = "real-time-indicator",
bs_icon("lightning-charge"), " LIVE REACTIVE TRACKING"
),
titlePanel(
div(style = "text-align: center; margin-bottom: 30px;",
h1(bs_icon("diagram-3"), "Reactive Dependency Graph Visualizer",
style = "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
font-weight: bold;"),
p("Watch Shiny's reactive magic happen in real-time with execution flow tracking",
class = "lead", style = "color: #6c757d;")
)
),
# Control Panel
div(class = "control-panel",
h4(bs_icon("sliders"), "Reactive Input Sources"),
fluidRow(
column(3,
sliderInput("n_points", "Sample Size:",
min = 10, max = 200, value = 50, step = 10,
animate = animationOptions(interval = 1000))
),
column(3,
selectInput("dataset", "Distribution:",
choices = c("Normal" = "normal",
"Uniform" = "uniform",
"Exponential" = "exp",
"Beta" = "beta"))
),
column(3,
numericInput("noise_level", "Noise Factor:",
value = 1, min = 0.1, max = 3, step = 0.2)
),
column(3,
sliderInput("threshold", "Analysis Threshold:",
min = -2, max = 2, value = 0, step = 0.1)
)
)
),
# Scenario Testing
div(class = "scenario-selector",
h4(bs_icon("play-circle"), "Test Reactive Scenarios"),
p("Experience different reactive patterns and see their execution flows:"),
fluidRow(
column(3,
actionButton("scenario_simple", "Simple Flow", class = "btn-scenario w-100")
),
column(3,
actionButton("scenario_complex", "Complex Chain", class = "btn-scenario w-100")
),
column(3,
actionButton("scenario_performance", "Performance Test", class = "btn-scenario w-100")
),
column(3,
actionButton("reset_all", "Reset Everything", class = "btn-scenario w-100")
)
)
),
# Dependency Graph Visualization
fluidRow(
column(4,
div(class = "dependency-node source-node",
h5(bs_icon("broadcast"), "INPUT SOURCES"),
p("User interactions that trigger the reactive chain"),
hr(),
div("Sample Size",
div(class = "execution-counter", textOutput("source_count_n", inline = TRUE)),
div(class = "timing-display", "Last: ", textOutput("timing_source", inline = TRUE))
),
br(),
div("Distribution Type",
div(class = "execution-counter", textOutput("source_count_dist", inline = TRUE)),
div(class = "timing-display", "Dependencies: 2 reactives")
),
br(),
div("Noise & Threshold",
div(class = "execution-counter", textOutput("source_count_params", inline = TRUE)),
div(class = "timing-display", "Triggers: Processing chain")
)
)
),
column(4,
div(class = "dependency-node conductor-node",
h5(bs_icon("arrow-repeat"), "REACTIVE CONDUCTORS"),
p("Intermediate calculations that cache and transform data"),
hr(),
div("Base Data Generator",
div(class = "execution-counter", textOutput("conductor_count_base", inline = TRUE)),
div(class = "timing-display", "Exec time: ", textOutput("timing_base", inline = TRUE))
),
br(),
div("Data Processor",
div(class = "execution-counter", textOutput("conductor_count_processed", inline = TRUE)),
div(class = "timing-display", "Cache: ", textOutput("cache_status", inline = TRUE))
),
br(),
div("Statistical Analysis",
div(class = "execution-counter", textOutput("conductor_count_stats", inline = TRUE)),
div(class = "timing-display", "Computations: 8 metrics")
)
)
),
column(4,
div(class = "dependency-node endpoint-node",
h5(bs_icon("bullseye"), "OUTPUT ENDPOINTS"),
p("Final renders that display results to users"),
hr(),
div("Interactive Plot",
div(class = "execution-counter", textOutput("endpoint_count_plot", inline = TRUE)),
div(class = "timing-display", "Render: ", textOutput("timing_plot", inline = TRUE))
),
br(),
div("Data Table",
div(class = "execution-counter", textOutput("endpoint_count_table", inline = TRUE)),
div(class = "timing-display", "Rows: ", textOutput("table_rows", inline = TRUE))
),
br(),
div("Summary Text",
div(class = "execution-counter", textOutput("endpoint_count_text", inline = TRUE)),
div(class = "timing-display", "Updates: Real-time")
)
)
)
),
# Performance Metrics Dashboard
div(class = "performance-metrics",
h4(bs_icon("speedometer2"), "Live Performance Analytics"),
fluidRow(
column(3,
div(class = "metric-card",
span(class = "metric-value", textOutput("total_executions", inline = TRUE)),
div("Total Executions")
)
),
column(3,
div(class = "metric-card",
span(class = "metric-value", textOutput("avg_execution_time", inline = TRUE)),
div("Avg Execution (ms)")
)
),
column(3,
div(class = "metric-card",
span(class = "metric-value", textOutput("cache_efficiency", inline = TRUE)),
div("Cache Efficiency")
)
),
column(3,
div(class = "metric-card",
span(class = "metric-value", textOutput("dependency_depth", inline = TRUE)),
div("Dependency Depth")
)
)
)
),
# Execution Flow Tracker
fluidRow(
column(6,
h4(bs_icon("activity"), "Real-Time Execution Flow"),
div(class = "execution-flow",
verbatimTextOutput("execution_flow_log")
)
),
column(6,
h4(bs_icon("graph-up"), "Dependency Performance"),
plotOutput("performance_plot", height = "300px")
)
),
# Output Panels
fluidRow(
column(8,
div(class = "output-panel",
h5("Live Data Visualization"),
plotOutput("main_plot", height = "400px")
)
),
column(4,
div(class = "output-panel",
h5("Statistical Summary"),
verbatimTextOutput("stats_summary"),
br(),
h6("Live Data Sample"),
DT::dataTableOutput("data_preview")
)
)
)
)
server <- function(input, output, session) {
# Reactive execution tracking
execution_log <- reactiveVal(character(0))
execution_times <- reactiveValues()
counters <- reactiveValues(
source_n = 0, source_dist = 0, source_params = 0,
conductor_base = 0, conductor_processed = 0, conductor_stats = 0,
endpoint_plot = 0, endpoint_table = 0, endpoint_text = 0,
total = 0
)
# Utility function to log executions with timing
log_execution <- function(type, name, start_time = NULL) {
timestamp <- format(Sys.time(), "%H:%M:%S.%OS3")
exec_time <- if (!is.null(start_time)) round((Sys.time() - start_time) * 1000, 1) else NA
entry <- paste0(timestamp, " [", type, "] ", name,
if (!is.null(start_time)) paste0(" (", exec_time, "ms)") else "")
current_log <- execution_log()
new_log <- c(tail(current_log, 19), entry) # Keep last 20 entries
execution_log(new_log)
if (!is.null(start_time)) {
execution_times[[name]] <- exec_time
}
counters$total <- counters$total + 1
# Add visual feedback
session$sendCustomMessage("highlight_node", list(type = type))
}
# Scenario handlers
observeEvent(input$scenario_simple, {
updateSliderInput(session, "n_points", value = 30)
updateSelectInput(session, "dataset", selected = "normal")
updateNumericInput(session, "noise_level", value = 0.5)
log_execution("SCENARIO", "Simple Flow Activated")
})
observeEvent(input$scenario_complex, {
updateSliderInput(session, "n_points", value = 150)
updateSelectInput(session, "dataset", selected = "beta")
updateNumericInput(session, "noise_level", value = 2.5)
updateSliderInput(session, "threshold", value = 1.5)
log_execution("SCENARIO", "Complex Chain Activated")
})
observeEvent(input$scenario_performance, {
updateSliderInput(session, "n_points", value = 200)
updateSelectInput(session, "dataset", selected = "exp")
updateNumericInput(session, "noise_level", value = 3)
log_execution("SCENARIO", "Performance Test Activated")
})
observeEvent(input$reset_all, {
for (name in names(counters)) {
counters[[name]] <- 0
}
execution_log(character(0))
execution_times <- reactiveValues()
log_execution("SYSTEM", "All Counters Reset")
})
# Source tracking
observeEvent(input$n_points, {
counters$source_n <- counters$source_n + 1
log_execution("SOURCE", "Sample Size Changed")
}, ignoreInit = TRUE)
observeEvent(input$dataset, {
counters$source_dist <- counters$source_dist + 1
log_execution("SOURCE", "Distribution Changed")
}, ignoreInit = TRUE)
observeEvent(c(input$noise_level, input$threshold), {
counters$source_params <- counters$source_params + 1
log_execution("SOURCE", "Parameters Updated")
}, ignoreInit = TRUE)
# Reactive conductors with clean separation (no counter updates inside)
base_data <- reactive({
req(input$n_points, input$dataset)
n <- input$n_points
set.seed(42) # Consistent results for demo
vals <- switch(input$dataset,
normal = rnorm(n),
uniform = runif(n, -2, 2),
exp = rexp(n, 1) - 1,
beta = rbeta(n, 2, 5) * 4 - 2)
data.frame(
id = 1:n,
value = vals,
category = sample(c("A", "B", "C"), n, replace = TRUE, prob = c(0.5, 0.3, 0.2))
)
})
processed_data <- reactive({
req(base_data(), input$noise_level)
data <- base_data()
data$value <- data$value + rnorm(nrow(data), 0, input$noise_level)
data$scaled_value <- scale(data$value)[,1]
data$transformed <- log1p(abs(data$value)) * sign(data$value)
data
})
statistical_analysis <- reactive({
req(processed_data(), input$threshold)
data <- processed_data()
list(
mean = mean(data$value),
median = median(data$value),
sd = sd(data$value),
q25 = quantile(data$value, 0.25),
q75 = quantile(data$value, 0.75),
above_threshold = sum(data$value > input$threshold),
category_means = tapply(data$value, data$category, mean),
correlation = cor(data$value, data$scaled_value)
)
})
# Safe counter tracking using observe() pattern with timing
observe({
start_time <- Sys.time()
base_data() # Access the reactive to trigger dependency
exec_time <- round((Sys.time() - start_time) * 1000, 1)
isolate({
counters$conductor_base <- counters$conductor_base + 1
execution_times[["Base Data Generated"]] <- exec_time
log_execution("CONDUCTOR", "Base Data Generated")
})
})
observe({
start_time <- Sys.time()
processed_data() # Access the reactive to trigger dependency
exec_time <- round((Sys.time() - start_time) * 1000, 1)
isolate({
counters$conductor_processed <- counters$conductor_processed + 1
execution_times[["Data Processed"]] <- exec_time
log_execution("CONDUCTOR", "Data Processed")
})
})
observe({
start_time <- Sys.time()
statistical_analysis() # Access the reactive to trigger dependency
exec_time <- round((Sys.time() - start_time) * 1000, 1)
isolate({
counters$conductor_stats <- counters$conductor_stats + 1
execution_times[["Statistics Computed"]] <- exec_time
log_execution("CONDUCTOR", "Statistics Computed")
})
})
# Track endpoint executions safely
observe({
start_time <- Sys.time()
processed_data() # Trigger when plot data is ready
exec_time <- round((Sys.time() - start_time) * 1000, 1)
isolate({
counters$endpoint_plot <- counters$endpoint_plot + 1
execution_times[["Plot Rendered"]] <- exec_time
log_execution("ENDPOINT", "Plot Data Ready")
})
})
observe({
start_time <- Sys.time()
processed_data() # Trigger when table data is ready
exec_time <- round((Sys.time() - start_time) * 1000, 1)
isolate({
counters$endpoint_table <- counters$endpoint_table + 1
execution_times[["Table Updated"]] <- exec_time
log_execution("ENDPOINT", "Table Data Ready")
})
})
observe({
start_time <- Sys.time()
statistical_analysis() # Trigger when summary data is ready
exec_time <- round((Sys.time() - start_time) * 1000, 1)
isolate({
counters$endpoint_text <- counters$endpoint_text + 1
execution_times[["Summary Generated"]] <- exec_time
log_execution("ENDPOINT", "Summary Data Ready")
})
})
# Output endpoints without counter updates inside render functions
output$main_plot <- renderPlot({
req(processed_data(), statistical_analysis())
data <- processed_data()
stats <- statistical_analysis()
par(mfrow = c(1, 2), mar = c(4, 4, 3, 1))
# Scatter plot
plot(data$id, data$value,
col = rainbow(3)[as.factor(data$category)],
pch = 16, cex = 1.2,
main = "Reactive Data Visualization",
xlab = "Observation ID", ylab = "Value")
abline(h = stats$mean, col = "red", lwd = 2, lty = 2)
abline(h = input$threshold, col = "blue", lwd = 2, lty = 3)
legend("topright",
legend = c("Mean", "Threshold", "A", "B", "C"),
col = c("red", "blue", rainbow(3)),
pch = c(NA, NA, rep(16, 3)),
lty = c(2, 3, rep(NA, 3)))
# Histogram
hist(data$value, breaks = 20, col = "lightblue", border = "white",
main = "Distribution Analysis", xlab = "Value", ylab = "Frequency")
abline(v = stats$mean, col = "red", lwd = 2, lty = 2)
abline(v = input$threshold, col = "blue", lwd = 2, lty = 3)
})
output$data_preview <- DT::renderDataTable({
req(processed_data())
data <- head(processed_data(), 10)
data
}, options = list(pageLength = 5, searching = FALSE, info = FALSE))
output$stats_summary <- renderText({
req(statistical_analysis(), processed_data())
stats <- statistical_analysis()
data <- processed_data()
summary_text <- paste(
"📊 STATISTICAL SUMMARY",
"━━━━━━━━━━━━━━━━━━━━━━",
paste("Mean:", round(stats$mean, 3)),
paste("Median:", round(stats$median, 3)),
paste("Std Dev:", round(stats$sd, 3)),
paste("IQR:", round(stats$q25, 3), "to", round(stats$q75, 3)),
"",
"🎯 THRESHOLD ANALYSIS",
paste("Above threshold:", stats$above_threshold, "observations"),
paste("Percentage:", round(stats$above_threshold / nrow(data) * 100, 1), "%"),
"",
"📈 CATEGORY BREAKDOWN",
paste("Category A:", round(stats$category_means[["A"]], 3)),
paste("Category B:", round(stats$category_means[["B"]], 3)),
paste("Category C:", round(stats$category_means[["C"]], 3)),
sep = "\n"
)
summary_text
})
# Performance tracking outputs - with meaningful labels
output$performance_plot <- renderPlot({
times_list <- reactiveValuesToList(execution_times)
times <- unlist(times_list[!sapply(times_list, is.null)])
if (length(times) > 0) {
# Get the names of what was executed
execution_names <- names(times)
recent_times <- tail(times, 10)
recent_names <- tail(execution_names, 10)
# Shorten names for better display
display_names <- gsub("Data ", "", recent_names)
display_names <- gsub("Generated", "Gen", display_names)
display_names <- gsub("Processed", "Proc", display_names)
display_names <- gsub("Computed", "Comp", display_names)
display_names <- gsub("Rendered", "Rend", display_names)
display_names <- gsub("Updated", "Upd", display_names)
# Create color coding by type
colors <- ifelse(grepl("Base|Gen", display_names), "#ff6b6b", # Red for base data
ifelse(grepl("Proc|Statistics|Comp", display_names), "#4ecdc4", # Teal for processing
ifelse(grepl("Plot|Rend", display_names), "#45b7d1", # Blue for plots
ifelse(grepl("Table|Upd", display_names), "#96ceb4", # Green for tables
ifelse(grepl("Summary", display_names), "#feca57", "#dda0dd"))))) # Yellow for summary, purple for others
par(mar = c(6, 4, 4, 2))
barplot(recent_times,
main = "Reactive Execution Performance",
ylab = "Time (ms)",
col = colors,
las = 2,
names.arg = display_names,
cex.names = 0.8)
# Add legend
legend("topright",
legend = c("Data Gen", "Processing", "Plot Render", "Table Upd", "Summary"),
fill = c("#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57"),
cex = 0.7)
} else {
# Show placeholder when no data
plot(1, 1, type = "n", xlim = c(0, 10), ylim = c(0, 10),
main = "Reactive Execution Performance",
xlab = "Reactive Operation", ylab = "Time (ms)")
text(5, 5, "Interact with inputs to see\nexecution performance",
cex = 1.2, col = "gray", adj = 0.5)
}
})
# Real-time execution log - simplified approach without JavaScript
output$execution_flow_log <- renderText({
current_log <- execution_log()
if (length(current_log) > 0) {
paste(current_log, collapse = "\n")
} else {
"Waiting for reactive executions..."
}
})
# Counter outputs
output$source_count_n <- renderText(counters$source_n)
output$source_count_dist <- renderText(counters$source_dist)
output$source_count_params <- renderText(counters$source_params)
output$conductor_count_base <- renderText(counters$conductor_base)
output$conductor_count_processed <- renderText(counters$conductor_processed)
output$conductor_count_stats <- renderText(counters$conductor_stats)
output$endpoint_count_plot <- renderText(counters$endpoint_plot)
output$endpoint_count_table <- renderText(counters$endpoint_table)
output$endpoint_count_text <- renderText(counters$endpoint_text)
# Timing outputs with safety checks
output$timing_source <- renderText({
latest_time <- reactiveValuesToList(execution_times)[["Parameters Updated"]]
if (!is.null(latest_time)) {
paste(latest_time, "ms")
} else "Not executed"
})
output$timing_base <- renderText({
latest_time <- reactiveValuesToList(execution_times)[["Base Data Generated"]]
if (!is.null(latest_time)) {
paste(latest_time, "ms")
} else "Not executed"
})
output$timing_plot <- renderText({
if (counters$endpoint_plot > 0) {
"Rendered"
} else "Not executed"
})
output$cache_status <- renderText({
if (counters$conductor_processed > 0) "Active" else "Cold"
})
output$table_rows <- renderText({
data <- processed_data()
if (!is.null(data)) nrow(data) else "0"
})
# Performance metrics with safety
output$total_executions <- renderText({
as.character(counters$total)
})
output$avg_execution_time <- renderText({
times_list <- reactiveValuesToList(execution_times)
times <- unlist(times_list[!sapply(times_list, is.null)])
if (length(times) > 0) {
paste(round(mean(times), 1), "ms")
} else "0 ms"
})
output$cache_efficiency <- renderText({
total_renders <- counters$endpoint_plot + counters$endpoint_table + counters$endpoint_text
if (total_renders > 0 && counters$conductor_base > 0) {
efficiency <- round((1 - (counters$conductor_base / total_renders)) * 100)
paste0(max(0, efficiency), "%")
} else "0%"
})
output$dependency_depth <- renderText("3 levels")
# Real-time execution log with safety
observe({
current_log <- execution_log()
if (length(current_log) > 0) {
# Log is now handled by the renderText output
# No need for JavaScript messages
}
})
# Custom JavaScript for enhanced interactivity (simplified)
observe({
tags$script(HTML("
// Simple highlighting without complex message handling
$(document).ready(function() {
$('.dependency-node').hover(
function() { $(this).addClass('shadow-lg'); },
function() { $(this).removeClass('shadow-lg'); }
);
});
"))
})
}
shinyApp(ui = ui, server = server)
Debugging & Troubleshooting Cheatsheet - Reactive flow visualization, cascade debugging, and performance optimization patterns.
The Three Pillars of Reactive Programming
Shiny’s reactive system consists of three fundamental types of reactive components, each serving a specific purpose in the reactive ecosystem.
1. Reactive Sources: Where It All Begins
Reactive sources are the starting points of reactive chains. They generate values that other reactive components can depend on.
Built-in Reactive Sources:
- User inputs:
input$slider
,input$text
,input$button
- File system: File modification timestamps
- Timer sources:
invalidateLater()
,reactiveTimer()
Custom Reactive Sources:
# reactiveVal - single reactive value
<- reactiveVal(0)
counter
# Update the value
observeEvent(input$increment, {
counter(counter() + 1)
})
# Use in outputs
$count_display <- renderText({
outputpaste("Count:", counter())
})
# reactiveValues - multiple related values
<- reactiveValues(
state current_page = 1,
items_per_page = 10,
total_items = 0,
selected_items = character(0)
)
# Update multiple values
observeEvent(input$next_page, {
$current_page <- state$current_page + 1
state
})
observeEvent(input$page_size, {
$items_per_page <- input$page_size
state$current_page <- 1 # Reset to first page
state })
2. Reactive Conductors: Processing and Transformation
Reactive conductors take reactive sources (or other conductors) and transform them into new reactive values. They’re the workhorses of reactive programming.
Basic Reactive Expressions:
<- function(input, output, session) {
server
# Simple reactive expression
<- reactive({
selected_data switch(input$dataset,
"mtcars" = mtcars,
"iris" = iris,
"airquality" = airquality)
})
# Dependent reactive expression
<- reactive({
filtered_data <- selected_data()
data
if (input$filter_enabled) {
# Apply filtering logic
$filter_column]] > input$filter_value, ]
data[data[[inputelse {
}
data
}
})
# Complex processing reactive
<- reactive({
analysis_results <- filtered_data()
data
list(
summary_stats = summary(data),
correlation_matrix = cor(data[sapply(data, is.numeric)]),
row_count = nrow(data),
column_info = sapply(data, class)
)
}) }
Event-Reactive Expressions:
# eventReactive - only updates when specific events occur
<- eventReactive(input$run_analysis, {
analysis_results # This only runs when the button is clicked
expensive_analysis(filtered_data())
})
# Can depend on multiple events
<- eventReactive(c(input$generate_report, input$refresh_data), {
report_data create_comprehensive_report(analysis_results())
})
3. Reactive Endpoints: Where Reactions Culminate
Reactive endpoints consume reactive values and produce side effects - they’re where the reactive chain ends and actual changes happen in the application.
Render Functions (Output Endpoints):
# Plot output
$main_plot <- renderPlot({
output<- filtered_data()
data
ggplot(data, aes_string(x = input$x_var, y = input$y_var)) +
geom_point(alpha = 0.7, color = input$point_color) +
theme_minimal() +
labs(title = paste("Relationship between", input$x_var, "and", input$y_var))
})
# Table output with formatting
$data_table <- renderDT({
outputdatatable(
filtered_data(),
options = list(
pageLength = input$rows_per_page,
scrollX = TRUE,
searching = input$enable_search
),filter = if(input$column_filters) "top" else "none"
) })
Observer Functions (Side Effect Endpoints):
# Basic observer - runs when dependencies change
observe({
<- filtered_data()
data
# Update UI based on data
updateSelectInput(session, "x_var",
choices = names(data)[sapply(data, is.numeric)])
updateSelectInput(session, "y_var",
choices = names(data)[sapply(data, is.numeric)])
})
# Event observer - runs only when specific events occur
observeEvent(input$save_analysis, {
<- analysis_results()
results
# Save to file
saveRDS(results, file = paste0("analysis_", Sys.Date(), ".rds"))
# Show notification
showNotification("Analysis saved successfully!", type = "success")
})
Reactive Pattern Comparison Tool
Experience the differences between reactive(), observe(), and observeEvent():
- Experiment with inputs - Change the slider and click buttons to see different behaviors
- Watch execution logs - See when each reactive pattern executes and why
- Compare timing - Notice how reactive() executes immediately, observe() runs on changes, eventReactive() waits for events
- Test caching - See how reactive expressions cache results vs. observers that run side effects
- Clear logs to start fresh and observe patterns clearly
Key Learning: Each reactive pattern serves different purposes - expressions for computed values, observers for side effects, event-reactives for user control.
#| '!! 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
## file: app.R
# Main Application FIle
# Reactive Pattern Comparison Tool
# Applies safe counter updates and prevents reactive feedback loops
library(shiny)
library(bslib)
library(bsicons)
# Source UI and server files
source("ui.R")
source("server.R")
# Run the application
shinyApp(ui = ui, server = server)
## file: ui.R
ui <- fluidPage(
theme = bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
.pattern-card {
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
background: white;
}
.reactive-card { border-color: #0d6efd; background: #f8f9ff; }
.observe-card { border-color: #fd7e14; background: #fff8f0; }
.event-card { border-color: #20c997; background: #f0fff8; }
.log-area {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
font-family: monospace;
font-size: 0.9rem;
max-height: 200px;
overflow-y: auto;
}
.execution-badge {
background: #6c757d;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.75rem;
}
"))
),
titlePanel("Reactive Pattern Comparison Tool"),
# Input Controls
fluidRow(
column(12,
div(class = "pattern-card",
h5(bs_icon("sliders"), " Input Controls"),
fluidRow(
column(4,
sliderInput("value_input", "Data Value:",
min = 1, max = 100, value = 50)
),
column(4,
actionButton("trigger_event", "Trigger Event",
class = "btn-primary")
),
column(4,
actionButton("clear_logs", "Clear Logs",
class = "btn-outline-secondary")
)
)
)
)
),
# Pattern Demonstrations
fluidRow(
# reactive() Pattern
column(4,
div(class = "pattern-card reactive-card",
h5(bs_icon("arrow-clockwise"), " reactive() Expression"),
p("Computes values, can be called like functions, caches results"),
h6("Current Result: ", textOutput("reactive_result", inline = TRUE)),
h6("Execution Count: ",
span(class = "execution-badge", textOutput("reactive_count", inline = TRUE))),
br(),
h6("Execution Log:"),
div(class = "log-area", verbatimTextOutput("reactive_log"))
)
),
# observe() Pattern
column(4,
div(class = "pattern-card observe-card",
h5(bs_icon("eye"), " observe() Side Effects"),
p("Performs side effects, cannot be called, runs when dependencies change"),
h6("Last Action: ", textOutput("observe_action", inline = TRUE)),
h6("Execution Count: ",
span(class = "execution-badge", textOutput("observe_count", inline = TRUE))),
br(),
h6("Execution Log:"),
div(class = "log-area", verbatimTextOutput("observe_log"))
)
),
# eventReactive() Pattern
column(4,
div(class = "pattern-card event-card",
h5(bs_icon("play-circle"), " eventReactive() Control"),
p("Waits for specific events, user-controlled execution"),
h6("Current Result: ", textOutput("event_result", inline = TRUE)),
h6("Execution Count: ",
span(class = "execution-badge", textOutput("event_count", inline = TRUE))),
br(),
h6("Execution Log:"),
div(class = "log-area", verbatimTextOutput("event_log"))
)
)
),
hr(),
# Comparison Summary
fluidRow(
column(12,
div(class = "pattern-card",
h5(bs_icon("table"), " Pattern Comparison Summary"),
tableOutput("comparison_table")
)
)
)
)
## file: server.R
server <- function(input, output, session) {
logs <- reactiveValues(reactive = character(0), observe = character(0), event = character(0))
counters <- reactiveValues(reactive = 0, observe = 0, event = 0)
observe_status <- reactiveValues(action = "Not yet triggered")
add_log <- function(type, message) {
timestamp <- format(Sys.time(), "%H:%M:%S")
entry <- paste(timestamp, "-", message)
isolate({
logs[[type]] <- tail(c(logs[[type]], entry), 10)
})
}
observeEvent(input$clear_logs, {
logs$reactive <- character(0)
logs$observe <- character(0)
logs$event <- character(0)
counters$reactive <- 0
counters$observe <- 0
counters$event <- 0
observe_status$action <- "Not yet triggered"
})
computed_value <- reactive({
add_log("reactive", paste("Computing value^2 =", input$value_input^2))
Sys.sleep(0.1)
input$value_input^2
})
observeEvent(computed_value(), {
counters$reactive <- counters$reactive + 1
})
observe({
current_value <- input$value_input
isolate({ counters$observe <- counters$observe + 1 })
action <- if (current_value < 25) "Value is LOW" else if (current_value > 75) "Value is HIGH" else "Value is MEDIUM"
add_log("observe", paste("Side effect triggered:", action))
observe_status$action <- action
})
event_computation <- eventReactive(input$trigger_event, {
add_log("event", paste("Event triggered, processing value:", input$value_input))
Sys.sleep(0.1)
sqrt(input$value_input)
})
# Track event execution count separately (to avoid renderText() conflict)
observeEvent(input$trigger_event, {
counters$event <- counters$event + 1
})
output$reactive_result <- renderText({ computed_value() })
output$observe_action <- renderText({ observe_status$action })
output$event_result <- renderText({
req(input$trigger_event > 0) # Safely guard rendering
round(event_computation(), 2)
})
output$reactive_count <- renderText(counters$reactive)
output$observe_count <- renderText(counters$observe)
output$event_count <- renderText(counters$event)
output$reactive_log <- renderText({
if (length(logs$reactive) == 0) "No executions yet" else paste(logs$reactive, collapse = "\n")
})
output$observe_log <- renderText({
if (length(logs$observe) == 0) "No executions yet" else paste(logs$observe, collapse = "\n")
})
output$event_log <- renderText({
if (length(logs$event) == 0) "No executions yet" else paste(logs$event, collapse = "\n")
})
output$comparison_table <- renderTable({
data.frame(
Pattern = c("reactive()", "observe()", "eventReactive()"),
Purpose = c("Compute values", "Side effects", "User-controlled computation"),
"Can be called" = c("Yes", "No", "Yes"),
"Returns value" = c("Yes", "No", "Yes"),
"Caches result" = c("Yes", "N/A", "Yes"),
"Execution trigger" = c("Dependency change", "Dependency change", "Specific event"),
"Times executed" = c(counters$reactive, counters$observe, counters$event),
check.names = FALSE
)
})
}
Advanced Reactive Patterns and Techniques
Once you understand the basics, these advanced patterns will help you build more sophisticated and efficient applications.
Conditional Reactivity with req() and validate()
Control when reactive expressions execute and provide user-friendly error handling:
<- function(input, output, session) {
server
# Use req() to prevent execution until conditions are met
<- reactive({
filtered_data # Wait for all required inputs
req(input$dataset)
req(input$filter_column)
req(input$filter_value)
<- get_dataset(input$dataset)
data $filter_column]] > input$filter_value, ]
data[data[[input
})
# Use validate() for user-friendly error messages
$analysis_plot <- renderPlot({
output<- filtered_data()
data
validate(
need(nrow(data) > 0, "No data matches the current filter criteria."),
need(ncol(data) > 1, "Dataset must have at least 2 columns for analysis."),
need(input$x_var %in% names(data), "Selected X variable not found in data.")
)
create_analysis_plot(data, input$x_var, input$y_var)
}) }
Reactive Values for Complex State Management
Use reactiveValues()
to manage complex application state that doesn’t fit the simple input-output model:
<- function(input, output, session) {
server
# Complex application state
<- reactiveValues(
app_state # Data management
raw_data = NULL,
processed_data = NULL,
# UI state
current_tab = "data_input",
show_advanced_options = FALSE,
# Analysis state
selected_models = character(0),
model_results = list(),
# User preferences
theme = "default",
language = "en"
)
# Initialize application
observe({
$raw_data <- load_default_data()
app_state$theme <- get_user_preference("theme", "default")
app_state
})
# Complex state updates
observeEvent(input$process_data, {
req(app_state$raw_data)
# Update processing status
$current_tab <- "processing"
app_state
# Process data
<- complex_data_processing(
processed $raw_data,
app_stateoptions = get_processing_options()
)
$processed_data <- processed
app_state$current_tab <- "results"
app_state
# Update UI to reflect new state
updateTabsetPanel(session, "main_tabs", selected = "results")
}) }
Reactive Polling and Real-Time Updates
Create applications that update automatically based on external data sources:
<- function(input, output, session) {
server
# Reactive timer for periodic updates
<- reactiveTimer(intervalMs = 5000) # 5 seconds
autoUpdate
# Real-time data source
<- reactive({
live_data # Depend on timer to trigger updates
autoUpdate()
# Only update if auto-update is enabled
req(input$enable_auto_update)
# Fetch fresh data
fetch_live_data_from_api()
})
# File system polling
<- reactive({
file_data # Check file modification time
<- file.info(input$data_file$datapath)
file_info
# Only reload if file has changed
req(file_info$mtime > last_modified_time)
read.csv(input$data_file$datapath)
})
# Manual refresh control
<- reactiveVal(0)
manual_refresh
observeEvent(input$refresh_button, {
manual_refresh(manual_refresh() + 1)
})
# Combined reactive data source
<- reactive({
current_data # Depend on manual refresh trigger
manual_refresh()
if (input$data_source == "live") {
live_data()
else if (input$data_source == "file") {
} file_data()
else {
} static_data()
}
}) }
Breaking Reactive Dependencies with isolate()
Sometimes you need to access reactive values without creating dependencies:
<- function(input, output, session) {
server
# Counter that updates independently
<- reactiveVal(0)
click_count
# Update counter without creating dependency on the output
observeEvent(input$button, {
click_count(click_count() + 1)
})
# Output that shows current time but doesn't update automatically
$timestamp_display <- renderText({
output# This creates a dependency on click_count
<- click_count()
count
# This does NOT create a dependency on system time
<- isolate(Sys.time())
current_time
paste("Button clicked", count, "times. Last update:", current_time)
})
# Reactive expression that combines dependent and independent values
<- reactive({
analysis_result # These create dependencies
<- filtered_data()
data <- input$analysis_params
params
# These don't create dependencies (won't trigger recalculation)
<- isolate(input$notes)
user_notes <- isolate(Sys.time())
timestamp
# Perform analysis
<- run_analysis(data, params)
result
# Include metadata without creating dependencies
list(
result = result,
metadata = list(
notes = user_notes,
timestamp = timestamp,
user = isolate(session$user)
)
)
}) }
Advanced Reactive Patterns Playground
Explore sophisticated reactive control techniques:
- Test req() validation - See how reactive expressions wait for valid inputs
- Experiment with isolate() - Observe how to access values without creating dependencies
- Try validate() messaging - Experience user-friendly error handling
- Use eventReactive() control - Practice user-controlled computation timing
- Watch execution flows - Understand when and why reactive expressions execute
Key Learning: Advanced patterns give you precise control over reactive behavior, enabling robust, user-friendly 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: 1150
#| editorHeight: 300
## file: app.R
# Main Application FIle
# Advanced Reactive Patterns Playground
# Demonstrates req(), validate(), isolate(), and eventReactive() patterns
# Load necessary libraries
library(shiny)
library(bslib)
library(bsicons)
# Source UI and server files
source("ui.R")
source("server.R")
# Run the application
shinyApp(ui = ui, server = server)
## file: ui.R
ui <- fluidPage(
theme = bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
.technique-card {
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
background: white;
}
.req-card { border-color: #dc3545; background: #fdf2f2; }
.validate-card { border-color: #fd7e14; background: #fff8f0; }
.isolate-card { border-color: #6f42c1; background: #f8f6ff; }
.event-card { border-color: #20c997; background: #f0fff8; }
.output-area {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
min-height: 80px;
}
"))
),
titlePanel("Advanced Reactive Patterns Playground"),
# Main Controls
fluidRow(
column(12,
div(class = "technique-card",
h5(bs_icon("gear"), " Control Panel"),
fluidRow(
column(3,
numericInput("sample_size", "Sample Size:",
value = NULL, min = 1, max = 1000)
),
column(3,
selectInput("data_type", "Data Type:",
choices = c("Choose..." = "", "Normal" = "normal",
"Uniform" = "uniform", "Exponential" = "exp"))
),
column(3,
numericInput("threshold", "Threshold:",
value = 50, min = 0, max = 100)
),
column(3,
actionButton("compute_advanced", "Compute Advanced Analysis",
class = "btn-success")
)
)
)
)
),
# Pattern Demonstrations
fluidRow(
# req() Pattern
column(6,
div(class = "technique-card req-card",
h5(bs_icon("shield-check"), " req() - Conditional Execution"),
p("Waits for all required inputs before executing"),
h6("Required: Sample Size > 0 AND Data Type selected"),
div(class = "output-area",
verbatimTextOutput("req_output")
)
)
),
# validate() Pattern
column(6,
div(class = "technique-card validate-card",
h5(bs_icon("exclamation-triangle"), " validate() - User-Friendly Errors"),
p("Provides helpful error messages for invalid states"),
h6("Validation Rules: Sample Size 10-500, Valid Data Type"),
div(class = "output-area",
verbatimTextOutput("validate_output")
)
)
)
),
fluidRow(
# isolate() Pattern
column(6,
div(class = "technique-card isolate-card",
h5(bs_icon("lock"), " isolate() - Break Dependencies"),
p("Access values without creating reactive dependencies"),
h6("Updates only when Sample Size changes (Threshold isolated)"),
div(class = "output-area",
verbatimTextOutput("isolate_output")
)
)
),
# eventReactive() Pattern
column(6,
div(class = "technique-card event-card",
h5(bs_icon("play-circle"), " eventReactive() - User Control"),
p("Only executes when button is clicked"),
h6("Click 'Compute Advanced Analysis' to trigger"),
div(class = "output-area",
verbatimTextOutput("event_output")
)
)
)
),
hr(),
# Execution Summary
fluidRow(
column(12,
div(class = "technique-card",
h5(bs_icon("activity"), " Execution Summary"),
tableOutput("execution_summary")
)
)
)
)
## file: server.R
server <- function(input, output, session) {
execution_counts <- reactiveValues(
req_pattern = 0,
validate_pattern = 0,
isolate_pattern = 0,
event_pattern = 0
)
# 1. req() pattern
req_analysis <- reactive({
req(input$sample_size, input$sample_size > 0, input$data_type, input$data_type != "")
isolate({ execution_counts$req_pattern <- execution_counts$req_pattern + 1 })
set.seed(42)
data <- switch(input$data_type,
"normal" = rnorm(input$sample_size),
"uniform" = runif(input$sample_size, -2, 2),
"exp" = rexp(input$sample_size, 1))
paste(
"\u2713 req() conditions satisfied",
paste("Sample size:", input$sample_size),
paste("Data type:", input$data_type),
paste("Mean:", round(mean(data), 3)),
paste("SD:", round(sd(data), 3)),
sep = "\n"
)
})
# 2. validate() pattern
validate_analysis <- reactive({
isolate({ execution_counts$validate_pattern <- execution_counts$validate_pattern + 1 })
validate(
need(input$sample_size, "Please enter a sample size"),
need(is.numeric(input$sample_size), "Sample size must be a number"),
need(input$sample_size >= 10, "Sample size must be at least 10"),
need(input$sample_size <= 500, "Sample size too large (max 500)"),
need(input$data_type, "Please select a data type"),
need(input$data_type %in% c("normal", "uniform", "exp"), "Invalid data type selected")
)
set.seed(42)
data <- switch(input$data_type,
"normal" = rnorm(input$sample_size),
"uniform" = runif(input$sample_size, -2, 2),
"exp" = rexp(input$sample_size, 1))
paste(
"\u2713 Validation passed",
paste("Sample:", input$sample_size, "observations"),
paste("Distribution:", input$data_type),
paste("Range:", round(min(data), 2), "to", round(max(data), 2)),
sep = "\n"
)
})
# 3. isolate() pattern
isolate_analysis <- reactive({
req(input$sample_size, input$sample_size > 0, input$data_type)
isolate({ execution_counts$isolate_pattern <- execution_counts$isolate_pattern + 1 })
set.seed(42)
data <- switch(input$data_type,
"normal" = rnorm(input$sample_size),
"uniform" = runif(input$sample_size, -2, 2),
"exp" = rexp(input$sample_size, 1))
threshold_value <- isolate(input$threshold)
above_threshold <- sum(data > threshold_value)
paste(
"Dependencies tracked:",
paste("- Sample size:", input$sample_size),
paste("- Data type:", input$data_type),
"",
"Isolated access (no dependency):",
paste("- Threshold:", threshold_value),
paste("- Values above:", above_threshold),
paste("- Percentage:", round(above_threshold / length(data) * 100, 1), "%"),
"",
"Note: Changing threshold won't trigger update",
sep = "\n"
)
})
# 4. eventReactive() pattern + external counter
event_analysis <- eventReactive(input$compute_advanced, {
req(input$sample_size, input$data_type)
set.seed(sample(1:1000, 1))
data <- switch(input$data_type,
"normal" = rnorm(input$sample_size),
"uniform" = runif(input$sample_size, -2, 2),
"exp" = rexp(input$sample_size, 1))
analysis <- list(
mean = mean(data),
median = median(data),
q25 = quantile(data, 0.25),
q75 = quantile(data, 0.75),
skewness = sum((data - mean(data))^3) / (length(data) * sd(data)^3)
)
paste(
"Advanced Analysis Results:",
paste("Mean:", round(analysis$mean, 3)),
paste("Median:", round(analysis$median, 3)),
paste("Q25:", round(analysis$q25, 3)),
paste("Q75:", round(analysis$q75, 3)),
paste("Skewness:", round(analysis$skewness, 3)),
"",
"Only executes when button clicked",
sep = "\n"
)
})
observeEvent(input$compute_advanced, {
execution_counts$event_pattern <- execution_counts$event_pattern + 1
})
# Output renderers
output$req_output <- renderText({
tryCatch(req_analysis(), error = function(e) "Waiting for required inputs...")
})
output$validate_output <- renderText({ validate_analysis() })
output$isolate_output <- renderText({
tryCatch(isolate_analysis(), error = function(e) "Waiting for required inputs...")
})
output$event_output <- renderText({
req(input$compute_advanced > 0)
event_analysis()
})
output$execution_summary <- renderTable({
data.frame(
Pattern = c("req()", "validate()", "isolate()", "eventReactive()"),
Purpose = c("Conditional execution", "User-friendly errors", "Break dependencies", "User-controlled timing"),
"Execution Count" = c(
execution_counts$req_pattern,
execution_counts$validate_pattern,
execution_counts$isolate_pattern,
execution_counts$event_pattern
),
"Current Status" = c(
if (is.null(input$sample_size) || input$data_type == "") "Waiting" else "Active",
if (is.null(input$sample_size) || input$data_type == "") "Waiting" else "Active",
if (is.null(input$sample_size) || input$data_type == "") "Waiting" else "Active",
if (execution_counts$event_pattern == 0) "Ready" else "Executed"
),
check.names = FALSE
)
})
}
Reactive Programming Best Practices
Design Efficient Reactive Chains
Good Practice - Linear Chain:
# Efficient: Clear dependency chain
<- reactive({ load_data(input$source) })
raw_data <- reactive({ clean_data(raw_data()) })
cleaned_data <- reactive({ analyze_data(cleaned_data()) })
analyzed_data $plot <- renderPlot({ plot_data(analyzed_data()) }) output
Avoid - Diamond Dependencies:
# Less efficient: Complex dependency patterns
<- reactive({ expensive_computation(input$params) })
base_data
# These both depend on base_data but might recalculate unnecessarily
<- reactive({ transform_a(base_data()) })
branch_a <- reactive({ transform_b(base_data()) })
branch_b
# This depends on both branches
$combined <- renderPlot({
outputcombine_results(branch_a(), branch_b())
})
Optimize Performance with Proper Caching
<- function(input, output, session) {
server
# Expensive computation with smart caching
<- reactive({
expensive_result # Only recalculate when key parameters change
<- list(
key_params dataset = input$dataset,
algorithm = input$algorithm,
parameters = input$key_parameters
)
# Use req() to avoid unnecessary computation
req(all(lengths(key_params) > 0))
# Expensive operation
run_complex_analysis(key_params)
})
# Multiple outputs using cached result
$plot1 <- renderPlot({
output<- expensive_result() # Uses cached value
result create_plot1(result)
})
$plot2 <- renderPlot({
output<- expensive_result() # Uses same cached value
result create_plot2(result)
})
$summary <- renderText({
output<- expensive_result() # Still uses cached value
result summarize_results(result)
}) }
Handle Errors Gracefully
<- function(input, output, session) {
server
# Robust data loading with error handling
<- reactive({
safe_data tryCatch({
# Validate inputs first
validate(
need(input$data_file, "Please upload a data file"),
need(tools::file_ext(input$data_file$name) %in% c("csv", "xlsx"),
"File must be CSV or Excel format")
)
# Attempt to load data
if (tools::file_ext(input$data_file$name) == "csv") {
read.csv(input$data_file$datapath)
else {
} ::read_excel(input$data_file$datapath)
readxl
}
error = function(e) {
}, # Provide user-friendly error message
validate(paste("Error loading file:", e$message))
})
})
# Safe analysis with fallback
<- reactive({
analysis_result <- safe_data()
data
tryCatch({
perform_analysis(data, input$analysis_options)
error = function(e) {
}, # Return default result on error
list(
error = TRUE,
message = paste("Analysis failed:", e$message),
fallback_result = simple_summary(data)
)
})
})
# Output with error handling
$analysis_display <- renderUI({
output<- analysis_result()
result
if (isTRUE(result$error)) {
div(
class = "alert alert-warning",
h4("Analysis Error"),
p(result$message),
p("Showing basic summary instead:"),
renderPrint({ result$fallback_result })
)else {
} # Normal result display
render_analysis_output(result)
}
}) }
Debugging Reactive Applications
Using Reactive Log
Enable reactive logging to understand your application’s reactive behavior:
# Enable reactive logging (development only)
options(shiny.reactlog = TRUE)
# In your application
<- function(input, output, session) {
server # Your reactive code here
}
# After running your app, view the reactive log
::reactlogShow() shiny
Adding Debug Information
<- function(input, output, session) {
server
# Add debug output to track reactive execution
<- reactive({
debug_reactive cat("Debug: Processing data at", as.character(Sys.time()), "\n")
cat("Debug: Input dataset is", input$dataset, "\n")
<- process_data(input$dataset)
result
cat("Debug: Processed", nrow(result), "rows\n")
result
})
# Use reactive triggers to understand execution flow
observe({
cat("Observer triggered: dataset changed to", input$dataset, "\n")
})
observeEvent(input$process_button, {
cat("Event observer: Process button clicked\n")
}) }
Common Debugging Patterns
# Pattern 1: Validate reactive chain
<- reactive({
validate_chain cat("Step 1: Raw data\n")
<- raw_data()
raw print(str(raw))
cat("Step 2: Processed data\n")
<- process_data(raw)
processed print(str(processed))
cat("Step 3: Final result\n")
<- final_processing(processed)
result print(str(result))
result
})
# Pattern 2: Track reactive dependencies
$debug_info <- renderText({
outputpaste("Dependencies updated at:", Sys.time(),
"Dataset:", input$dataset,
"Filters:", paste(input$filters, collapse = ", "))
})
Common Issues and Solutions
Issue 1: Infinite Reactive Loops
Problem: Reactive expressions that depend on values they modify, creating endless update cycles.
Solution:
# BAD: Creates infinite loop
<- reactiveValues(counter = 0)
values
observe({
# This observer modifies the value it depends on!
$counter <- values$counter + 1 # INFINITE LOOP!
values
})
# GOOD: Use event-driven updates
<- reactiveValues(counter = 0)
values
observeEvent(input$increment_button, {
# Only updates when button is clicked
$counter <- values$counter + 1
values
})
# GOOD: Use isolate to break dependency
observe({
# Some condition that should trigger update
req(input$trigger_update)
# Update without creating dependency
<- isolate(values$counter)
current_value $counter <- current_value + 1
values })
Issue 2: Performance Problems with Expensive Reactive Expressions
Problem: Expensive computations running too frequently or unnecessarily.
Solution:
# BAD: Expensive computation runs on every input change
$expensive_plot <- renderPlot({
output# This runs every time ANY input changes
<- very_expensive_computation(input$dataset, input$params)
expensive_data create_plot(expensive_data)
})
# GOOD: Cache expensive computation in reactive expression
<- reactive({
expensive_data # Only recalculates when specific inputs change
very_expensive_computation(input$dataset, input$params)
})
$plot1 <- renderPlot({
outputcreate_plot1(expensive_data()) # Uses cached result
})
$plot2 <- renderPlot({
outputcreate_plot2(expensive_data()) # Uses same cached result
})
# EVEN BETTER: Use eventReactive for user-controlled updates
<- eventReactive(input$run_analysis, {
expensive_data very_expensive_computation(input$dataset, input$params)
})
Issue 3: Reactive Expressions Not Updating
Problem: Reactive expressions that should update but don’t respond to input changes.
Solution:
# Common causes and fixes:
# Cause 1: Missing reactive context
# BAD
<- function() {
non_reactive_data switch(input$dataset, # This won't work outside reactive context
"mtcars" = mtcars,
"iris" = iris)
}
# GOOD
<- reactive({
reactive_data switch(input$dataset, # This works in reactive context
"mtcars" = mtcars,
"iris" = iris)
})
# Cause 2: Using isolate() incorrectly
# BAD: isolate prevents reactivity
<- reactive({
filtered_data <- base_data()
data <- isolate(input$filter) # Won't update when filter changes!
filter_value $value > filter_value, ]
data[data
})
# GOOD: Don't isolate values you want to react to
<- reactive({
filtered_data <- base_data()
data <- input$filter # Will update when filter changes
filter_value $value > filter_value, ]
data[data
})
# Cause 3: req() preventing execution
# Check if req() conditions are too strict
<- reactive({
problematic_reactive req(input$value > 0) # Might never be true
process_data(input$value)
})
# Better: More lenient conditions or default values
<- reactive({
better_reactive <- input$value
value if (is.null(value) || value <= 0) {
<- 1 # Use default value
value
}process_data(value)
})
Common Questions About Reactive Programming in Shiny
reactive()
creates reactive expressions that return values and can be used by other reactive components. They’re lazy (only execute when needed) and cache results.
observe()
creates observers that perform side effects but don’t return values. They execute immediately when their dependencies change and can’t be called like functions.
observeEvent()
creates event observers that only execute when specific events occur, giving you precise control over when reactions happen.
# reactive() - returns a value, can be called
<- reactive({ process_data(input$file) })
data_processed <- data_processed() # Can call like a function
result
# observe() - side effects only, can't be called
observe({
updateSelectInput(session, "columns", choices = names(data_processed()))
})
# observeEvent() - runs only when specific events occur
observeEvent(input$save_button, {
save_data(data_processed(), input$filename)
})
Use reactive()
for data processing and calculations that other components need. Use observe()
for UI updates and automatic side effects. Use observeEvent()
for user-triggered actions and precise event control.
reactiveVal()
is for single reactive values that you need to read and update programmatically. It’s like a reactive variable.
reactiveValues()
is for multiple related reactive values that form a reactive object with named components.
reactive()
is for computed values based on other reactive sources - it’s read-only and recalculates automatically.
# reactiveVal - single value you control
<- reactiveVal(0)
counter counter(counter() + 1) # Update
<- counter() # Read
current_count
# reactiveValues - multiple related values
<- reactiveValues(page = 1, items = 10, data = NULL)
state $page <- 2 # Update
state<- state$page # Read
current_page
# reactive - computed from other sources
<- reactive({
filtered_data raw_data()[raw_data()$category == input$filter, ]
})
Use reactiveVal()
for simple state like counters, flags, or single values you programmatically update. Use reactiveValues()
for complex state with multiple related properties. Use reactive()
for computed values derived from inputs or other reactive sources.
Use req()
to prevent execution until conditions are met:
<- reactive({
analysis req(input$file) # Wait for file upload
req(input$columns) # Wait for column selection
req(length(input$columns) > 0) # Ensure columns selected
expensive_analysis(input$file, input$columns)
})
Use eventReactive()
for user-controlled updates:
<- eventReactive(input$run_button, {
analysis expensive_analysis(input$data, input$params)
})
Use isolate()
to access values without creating dependencies:
<- reactive({
analysis <- input$data # Creates dependency
data <- isolate(Sys.time()) # No dependency
timestamp process_data(data, timestamp)
})
Use debounce()
or throttle()
** for frequently changing inputs:
# Debounce text input - only react after user stops typing
<- debounce(reactive(input$text_input), 1000) # 1 second delay stable_text
Enable reactive logging to see the execution flow:
options(shiny.reactlog = TRUE)
# Run your app, then:
::reactlogShow() shiny
Add debug output to track execution:
<- reactive({
debug_data cat("Processing data at", as.character(Sys.time()), "\n")
<- process_data(input$dataset)
result cat("Processed", nrow(result), "rows\n")
result })
Use browser()
for interactive debugging:
<- reactive({
problematic_reactive <- input_data()
data browser() # Execution will pause here
process_data(data)
})
Check reactive dependencies with systematic testing:
# Test each step in the reactive chain
$debug1 <- renderText({ paste("Input:", input$value) })
output$debug2 <- renderText({ paste("Processed:", processed_data()) })
output$debug3 <- renderText({ paste("Final:", final_result()) }) output
Common debugging patterns: Check that inputs exist, verify reactive context, ensure no infinite loops, and validate that req()
conditions aren’t too restrictive.
Reactive expressions cache results until dependencies change, making them very efficient for shared computations:
# EFFICIENT - computed once, used multiple times
<- reactive({ expensive_computation(input$params) })
shared_data $plot1 <- renderPlot({ plot1(shared_data()) })
output$plot2 <- renderPlot({ plot2(shared_data()) }) output
Direct computation in render functions repeats work unnecessarily:
# INEFFICIENT - computation repeated for each output
$plot1 <- renderPlot({ plot1(expensive_computation(input$params)) })
output$plot2 <- renderPlot({ plot2(expensive_computation(input$params)) }) output
Event-driven patterns can improve performance by reducing unnecessary updates:
# BETTER PERFORMANCE - only updates when user requests
<- eventReactive(input$analyze_button, {
analysis expensive_analysis(input$data, input$complex_params)
})
Memory considerations: Reactive expressions hold their cached values in memory. For large datasets, consider clearing cache periodically or using database connections instead of in-memory storage.
Best practices: Use reactive expressions for shared computations, avoid complex nested reactive chains, and use req()
to prevent unnecessary executions with invalid inputs.
Test Your Understanding
Which reactive pattern would be most appropriate for implementing a “Save Progress” feature that automatically saves user work every 30 seconds, but only if there have been changes since the last save?
reactive()
expression that checks for changes
observe()
withinvalidateLater()
and change detection
observeEvent()
triggered by a timer
reactiveTimer()
with conditional logic
- Consider the need for automatic timing combined with conditional execution
- Think about which pattern best handles both periodic execution and change detection
- Remember the differences between reactive expressions and observers
B) observe()
with invalidateLater()
and change detection
This pattern provides the most elegant solution for automatic saving with change detection:
<- function(input, output, session) {
server
# Track the last saved state
<- reactiveVal(NULL)
last_saved_state
# Current application state
<- reactive({
current_state list(
data = processed_data(),
settings = input$user_settings,
timestamp = input$last_modified
)
})
# Auto-save observer
observe({
# Set up 30-second timer
invalidateLater(30000) # 30 seconds
<- current_state()
current <- last_saved_state()
last_saved
# Only save if there are changes
if (!identical(current, last_saved)) {
# Perform save operation
save_user_progress(current)
# Update last saved state
last_saved_state(current)
# Show notification
showNotification("Progress saved automatically", type = "message")
}
}) }
Why this works best:
observe()
allows side effects (saving files, showing notifications)invalidateLater()
provides reliable 30-second intervals- Reactive dependency on
current_state()
ensures it tracks all relevant changes - Conditional logic prevents unnecessary saves when nothing has changed
Your Shiny app has an expensive data processing function that takes 5 seconds to run. Currently, it’s implemented as shown below. How would you optimize this for best performance?
<- function(input, output, session) {
server $plot <- renderPlot({
output<- expensive_processing(input$dataset, input$params)
processed_data create_plot(processed_data)
})
$table <- renderTable({
output<- expensive_processing(input$dataset, input$params)
processed_data create_table(processed_data)
})
$summary <- renderText({
output<- expensive_processing(input$dataset, input$params)
processed_data create_summary(processed_data)
}) }
- Use
isolate()
to prevent the function from running multiple times
- Create a
reactive()
expression for the expensive processing
- Use
eventReactive()
to only process when a button is clicked
- Move the processing to
global.R
to run only once
- Consider how many times the expensive function currently runs
- Think about caching and reusing computed results
- Remember the principle of shared reactive expressions
B) Create a reactive()
expression for the expensive processing
This optimization reduces the expensive computation from running 3 times to just once:
<- function(input, output, session) {
server
# Shared reactive expression - computed once, cached automatically
<- reactive({
processed_data expensive_processing(input$dataset, input$params)
})
# All outputs use the cached result
$plot <- renderPlot({
outputcreate_plot(processed_data()) # Uses cached result
})
$table <- renderTable({
outputcreate_table(processed_data()) # Uses same cached result
})
$summary <- renderText({
outputcreate_summary(processed_data()) # Still uses cached result
}) }
Why this is the best solution:
- Performance gain: Expensive function runs once instead of three times
- Automatic caching: Result is cached until
input$dataset
orinput$params
change - Maintains reactivity: All outputs still update when inputs change
- Clean architecture: Follows the principle of shared reactive expressions
Why other options are less optimal:
- Option A (
isolate()
): Would break reactivity, preventing updates when inputs change - Option C (
eventReactive()
): Adds unnecessary user interaction requirement - Option D (
global.R
): Wouldn’t be reactive to input changes, breaking app functionality
You’re building a data analysis app where users can select multiple analysis methods, and each analysis should only run when its specific “Run Analysis” button is clicked. The analyses depend on filtered data that should update automatically when filters change. What’s the best reactive architecture?
- One
eventReactive()
for all analyses triggered by all buttons
- Separate
eventReactive()
for each analysis, each depending on a shared filtered data reactive
observe()
functions that check which button was clicked
reactive()
expressions that useisolate()
to control execution
- Consider the need for both automatic updates (filtered data) and manual control (analysis execution)
- Think about how to combine reactive data dependencies with event-driven execution
- Remember the principle of separating concerns in reactive design
B) Separate eventReactive()
for each analysis, each depending on a shared filtered data reactive
This architecture provides the perfect balance of automatic reactivity and user control:
<- function(input, output, session) {
server
# Shared reactive for filtered data - updates automatically
<- reactive({
filtered_data req(input$dataset)
<- get_dataset(input$dataset)
data
if (input$apply_filters) {
<- data %>%
data filter(
>= input$min_value,
value %in% input$selected_categories,
category >= input$date_range[1],
date <= input$date_range[2]
date
)
}
data
})
# Separate event-reactive for each analysis type
<- eventReactive(input$run_regression, {
regression_analysis <- filtered_data() # Uses current filtered data
data perform_regression_analysis(data, input$regression_params)
})
<- eventReactive(input$run_clustering, {
clustering_analysis <- filtered_data() # Uses current filtered data
data perform_clustering_analysis(data, input$clustering_params)
})
<- eventReactive(input$run_timeseries, {
time_series_analysis <- filtered_data() # Uses current filtered data
data perform_timeseries_analysis(data, input$timeseries_params)
})
# Outputs for each analysis
$regression_results <- renderPlot({
outputreq(regression_analysis())
plot_regression(regression_analysis())
})
$clustering_results <- renderPlot({
outputreq(clustering_analysis())
plot_clusters(clustering_analysis())
})
$timeseries_results <- renderPlot({
outputreq(time_series_analysis())
plot_timeseries(time_series_analysis())
}) }
Why this architecture excels:
- Automatic data updates: Filtered data updates immediately when filters change
- User-controlled analysis: Each analysis only runs when its button is clicked
- Fresh data guarantee: Analyses always use the most current filtered data
- Independent execution: Each analysis can be run independently without affecting others
- Efficient caching: Each analysis result is cached until explicitly re-run
- Clear separation: Data filtering logic is separate from analysis execution logic
Architecture benefits:
- Predictable behavior: Users understand that changing filters updates data, clicking buttons runs analyses
- Performance optimization: Expensive analyses only run when requested
- Scalability: Easy to add new analysis types following the same pattern
Conclusion
Mastering reactive programming in Shiny transforms you from someone who builds functional applications to someone who builds elegant, efficient, and sophisticated interactive experiences. The reactive programming model - with its automatic dependency tracking, lazy evaluation, and caching mechanisms - provides a powerful foundation for creating applications that feel responsive and natural to users.
The concepts covered in this guide - from basic reactive expressions to advanced patterns like conditional reactivity, state management, and performance optimization - represent the core skills needed to build professional-grade Shiny applications. Understanding when to use reactive()
versus observe()
versus eventReactive()
, how to manage complex state with reactiveValues()
, and how to optimize performance through proper reactive design will serve you throughout your Shiny development career.
As you continue building applications, remember that reactive programming is both an art and a science. The technical patterns provide the tools, but knowing when and how to apply them comes with experience and practice.
Next Steps
Based on your mastery of reactive programming concepts, here are the recommended paths for advancing your Shiny development expertise:
Immediate Next Steps (Complete These First)
- Shiny Layout Systems and Design Patterns - Apply your reactive programming skills to create sophisticated, responsive user interfaces
- Complete Guide to Shiny Input Controls - Master the input widgets that trigger your reactive chains
- Practice Exercise: Refactor an existing application to use advanced reactive patterns like
eventReactive()
andreactiveValues()
for better performance and user control
Building on Your Foundation (Choose Your Path)
For Advanced Reactive Patterns:
For Performance Optimization:
For Interactive Features:
Long-term Goals (2-4 Weeks)
- Design and implement a complex reactive architecture for a multi-user application
- Create reusable reactive patterns that can be shared across different projects
- Optimize an existing application’s performance using advanced reactive programming techniques
- Build a real-time dashboard that demonstrates mastery of reactive programming principles
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 = {Mastering {Reactive} {Programming} in {Shiny:} {Complete}
{Guide}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/fundamentals/reactive-programming.html},
langid = {en}
}