Reactivity in Shiny
Reactivity is the heart of Shiny. It’s what makes your apps interactive—when users change inputs, outputs automatically update without you writing any explicit event handlers. Understanding how Shiny’s reactive system works is essential for building responsive and efficient applications.
This tutorial covers the fundamentals of reactivity: reactive values, reactive expressions, observers, and the execution lifecycle. You’ll learn not just how to use reactivity, but why it works the way it does.
Understanding Reactive Values
Reactive values are the foundation of interactivity in Shiny. They represent values that can change over time, typically in response to user actions. Every input from the UI—sliders, text boxes, selections—becomes a reactive value in the server function.
Accessing Input Values
Input values are accessed using the input object, which behaves like a reactive list:
server <- function(input, output, session) {
# Access input values using input$inputId
observeEvent(input$action_button, {
user_name <- input$user_name
selected_value <- input$selection
print(paste("User:", user_name, "selected:", selected_value))
})
}
The key insight is that reading input$something inside a reactive context creates a dependency. Shiny automatically tracks which outputs depend on which inputs.
Reactive Values Are Lazy
Reactive values only “fire” when accessed within a reactive context. If you read an input value in regular R code, it won’t trigger updates:
# This WON'T work as expected - not in a reactive context
current_value <- input$slider_value # Static at app start
print(current_value) # Will be NULL or initial value only
To work with reactive values properly, you must use them inside reactive consumers like render*() functions, reactive(), or observeEvent().
Reactive Expressions
Reactive expressions are reusable chunks of reactive code that cache their results. They’re created with the reactive() function and behave like functions—you call them with parentheses:
server <- function(input, output, session) {
# Create a reactive expression
filtered_data <- reactive({
req(input$selected_species) # Ensure input exists
iris_subset <- iris[iris$Species == input$selected_species, ]
return(iris_subset)
})
# Use the reactive expression in multiple outputs
output$summary_table <- renderTable({
summary(filtered_data())
})
output$row_count <- renderText({
nrow(filtered_data())
})
}
Why Use Reactive Expressions?
Reactive expressions provide three key benefits:
- Caching: If multiple outputs use the same calculation, it only runs once per user interaction
- Readability: They give names to complex transformations
- Efficiency: They only re-run when their dependencies change
# Without reactive expressions - calculation runs twice
output$plot1 <- renderPlot({
expensive_function(input$value) # Runs once
})
output$table1 <- renderTable({
expensive_function(input$value) # Runs again!
})
# With reactive expressions - calculation runs once
cached_result <- reactive({
expensive_function(input$value)
})
output$plot1 <- renderPlot({
cached_result()
})
output$table1 <- renderTable({
cached_result()
})
The Isolation Function
Sometimes you need to read a reactive value without creating a dependency. Use isolate() for this:
observeEvent(input$save_button, {
# This WON'T re-run when slider changes
current_value <- isolate(input$slider_value)
# But this WILL re-run when button clicked
save_to_database(current_value)
})
Understanding the Reactive Graph
Shiny builds a dependency graph at runtime. When an input changes, it invalidates everything that depends on it, creating an update cascade. Understanding this graph helps you debug and optimize your apps.
Topological Ordering
Shiny determines the correct order of execution based on dependencies:
server <- function(input, output, session) {
# Step 1: This runs first (no dependencies except input)
processed <- reactive({
input$raw_value * 2 + 10
})
# Step 2: This runs after processed()
output$result <- renderText({
paste("Result:", processed())
})
}
Invalidating the Cache
Every time an input changes, Shiny marks all dependent reactive expressions as invalidated. The next time they’re accessed, they re-execute:
# This reactive expression recalculates only when input$x or input$y changes
computation <- reactive({
input$x + input$y
})
output$display <- renderText({
# This block runs whenever computation() is invalidated
computation()
})
Observers and Side Effects
Observers are reactive statements that run for their side effects rather than returning values. They’re created with observeEvent() and observe().
Using observeEvent()
observeEvent() runs code in response to a specific trigger:
# Run this code when the button is clicked
observeEvent(input$submit_button, {
# Get values and perform action
data <- input$uploaded_file
result <- process_data(data)
# Update a reactive value or output
output$status <- renderText("Processing complete!")
# Show a notification
showNotification("Data processed successfully!", type = "message")
})
The Event Handler Arguments
observeEvent() accepts several arguments to control when it fires:
observeEvent(input$trigger, {
# Code to run
}, ignoreNULL = TRUE, # Don't run on first load (when trigger is NULL)
ignoreInit = TRUE, # Don't run when app first starts
once = FALSE, # If TRUE, only runs once ever
label = "my_observer" # For debugging
)
Using observe() for General Reactivity
observe() runs whenever any of its dependencies change:
# This runs whenever input$toggle changes
observe({
if (input$toggle) {
showElement("advanced_options")
} else {
hideElement("advanced_options")
}
})
The req() and validate() Functions
Requiring Values with req()
req() stops execution if its argument is NULL or empty—perfect for handling optional inputs:
output$plot <- renderPlot({
req(input$selected_column) # Stop if no column selected
data <- my_data[, input$selected_column]
hist(data)
})
You can also use req() to require multiple conditions:
observeEvent(input$calculate, {
req(input$value1, input$value2) # Both must be non-NULL
result <- input$value1 + input$value2
})
Validating with validate()
validate() shows a user-friendly error message instead of crashing:
output$analysis <- renderText({
# Check for errors and show appropriate message
validate(
need(input$category != "", "Please select a category"),
need(input$date_range > 0, "Date range must be positive")
)
# Only reaches here if validation passes
perform_analysis()
})
Common Reactivity Patterns
Debouncing and Throttling
For expensive operations, delay or limit how often calculations run:
# Only recalculate 500ms after user stops typing
debounced_value <- debounce(reactive(input$search_term), millis = 500)
output$results <- renderTable({
req(debounced_value())
search_database(debounced_value())
})
Conditional Panels
Show or hide UI elements based on reactive values:
ui <- fluidPage(
checkboxInput("show_advanced", "Show advanced options"),
conditionalPanel(
condition = "input.show_advanced == true",
sliderInput("advanced_slider", "Advanced Option", 1, 100, 50)
)
)
Loading States
Provide feedback during long-running operations:
output$heavy_computation <- renderText({
req(input$run_button)
# Show loading message
withProgress(message = "Computing...", {
setProgress(0.3)
result <- step_one()
setProgress(0.7)
result <- step_two(result)
})
return(result)
})
Debugging Reactivity
Reactivity can be tricky to debug. Here are essential techniques:
Using print() and browser()
server <- function(input, output, session) {
observe({
# Print to R console (check terminal)
print(paste("Slider value:", input$my_slider))
# Or use browser() for interactive debugging
# browser()
})
}
The reactlog Package
Visualize the reactive dependency graph:
# Install and enable
install.packages("reactlog")
shiny::reactlogEnable()
# Run your app, then press Ctrl+F3 in the app to see the graph
Understanding Invalidated State
Reactive expressions track whether they’re currently valid or invalidated:
debugged_reactive <- reactive({
message("Reactive expression running...")
input$x * 2
})
Performance Optimization
Reactive Timer for Polling
Update outputs at intervals:
# Re-render every 5 seconds
auto_invalidator <- reactiveTimer(5000)
observe({
auto_invalidator()
# This runs every 5 seconds
output$live_data <- renderText({
Sys.time()
})
})
Using isolate() Strategically
Prevent unnecessary recalculations:
observeEvent(input$save_button, {
# Only save, don't re-render anything
isolate({
save_data(input$data)
})
})
Summary
Reactivity is what makes Shiny powerful. Key concepts to remember:
- Reactive values (
input$...) represent user inputs and trigger updates - Reactive expressions (
reactive({})) cache computations and depend on their inputs - Observers (
observeEvent(),observe()) run code for side effects - req() ensures values exist before using them
- validate() provides user-friendly error messages
- The reactive graph determines execution order automatically
Mastering these concepts will let you build sophisticated, responsive Shiny applications that feel fast and professional.
Next Steps
Continue your Shiny journey:
- Reactive connectors: Use
reactiveConnector()for custom reactive sources - Modules: Build reusable reactive components with
moduleServer() - Advanced UI: Explore
renderUI()for dynamic interface elements - Custom outputs: Create your own
render*()functions for specialized outputs - Performance: Learn about
reactiveTimer(),invalidateLater(), and session management
With reactivity mastered, you can build any interactive application you envision.