Advanced Reactivity and Dependencies

Mark Andrews

Beyond simple reactive chains

Day 1 covered the basic reactive chain: input → reactive expression → output. Real applications often require:

  • Inputs whose choices depend on other inputs
  • Observers that trigger side effects
  • Dynamic UI that changes shape based on selections
  • Systematic debugging when something goes wrong

Dependent inputs: the problem

Suppose the user first selects a dataset, then selects a variable from that dataset. The variable choices should update when the dataset changes. A plain selectInput with fixed choices cannot do this.

observeEvent

observeEvent(input$dataset, {
  cols <- names(datasets[[input$dataset]])
  updateSelectInput(session, "xvar", choices = cols)
})
  • Runs its code block whenever input$dataset changes
  • update* functions modify existing controls without rebuilding the whole UI
  • Requires session as the third argument to the server function

The update* family

Function Modifies
updateSelectInput Choices and value
updateSliderInput Min, max, value
updateRadioButtons Choices and value
updateCheckboxGroupInput Choices and selected
updateTextInput Value and label

Pass session as the first argument after the input ID.

req(): guarding against NULL

output$plot <- renderPlot({
  req(input$xvar)
  ggplot(data, aes(x = .data[[input$xvar]])) + geom_histogram()
})
  • req(x) silently stops execution if x is NULL, NA, "", or FALSE
  • Prevents errors during initialisation before the user has interacted
  • Use it whenever an input might not yet have a valid value

renderUI: fully dynamic interface

output$params <- renderUI({
  switch(input$type,
    Normal = sliderInput("mu", "Mean", min = -5, max = 5, value = 0),
    Beta   = tagList(
      sliderInput("a", "Shape 1", min = 0.1, max = 10, value = 2),
      sliderInput("b", "Shape 2", min = 0.1, max = 10, value = 5)
    )
  )
})
  • uiOutput("params") in the UI is a placeholder for dynamically generated controls
  • renderUI returns UI elements as R objects
  • Use when the number or type of controls must change substantially

The reactive execution model

When input$x changes:

  1. Shiny marks every reactive expression and render block that reads input$x as invalidated
  2. On the next flush, each invalidated consumer re-executes in dependency order
  3. Reactive expressions are lazy: only re-run when a consumer actually calls them

observe({...}) is eager: re-runs immediately when its inputs change, even if nothing consumes it. Use it for side effects (logging, update* calls, writing files).

isolate(): suppressing reactivity

output$plot <- renderPlot({
  n <- input$n
  label <- isolate(input$label)  # changes to input$label do NOT trigger a redraw
  plot(rnorm(n), main = label)
})
  • isolate({...}) reads a reactive value without creating a dependency
  • The output updates when input$n changes but not when input$label changes
  • Useful for controls that should only take effect on a button press

Debugging: print statements

samples <- reactive({
  cat("Recomputing: n =", input$n, "\n")
  rnorm(input$n)
})
  • Output appears in the R console, not the browser
  • Quick and sufficient for most problems
  • Add a cat at the top of any block you are unsure about

Debugging: browser()

output$plot <- renderPlot({
  browser()
  ...
})
  • Suspends execution and opens the R debugger
  • Inspect input$n, local variables, reactive expression values
  • Remove before sharing or deploying

Debugging: reactlog

options(shiny.reactlog = TRUE)
# run the app, interact, then:
shiny::reactlogShow()
  • Visualises the entire reactive graph and its execution history
  • Shows which expressions ran, when, and why
  • Most powerful tool for understanding unexpected behaviour

Common errors and fixes

Error Cause Fix
“no active reactive context” Reading input$x outside a reactive Move into reactive, render*, or observe
Output blank on startup input$x is NULL initially Add req(input$x)
Output not updating Input inside isolate or not actually read Check the render block reads the input directly

Summary

  • observeEvent + update* functions handle dependent inputs
  • req() is the standard guard against NULL inputs
  • renderUI builds fully dynamic interfaces
  • Reactive expressions are lazy; observers are eager
  • isolate suppresses a reactive dependency explicitly
  • reactlog is the most powerful debugging tool