Shiny Reactivity

Key Components of Shiny Reactivity:

Reactive Inputs (input)

These are user interface elements like sliders, text inputs, checkboxes, and dropdowns. When a user interacts with these elements, their values change, and Shiny recognizes these changes as triggers for reactivity.

  • When to use this: Whenever you need to capture user preferences, selections, parameters, or raw text input into your Shiny application.

Example:

library(shiny)
ui <- fluidPage(
  sliderInput("num", "Choose a number:", min = 1, max = 100, value = 50)
)

When the user drags the slider from 50 to 75, input$num updates, notifying all downstream reactive components that depend on it.


Reactive Expressions (reactive(), eventReactive())

These are server expressions that depend on reactive inputs or other reactive expressions. They are “lazy,” meaning they only re-execute when their dependencies change and when their value is needed by an output or another reactive expression. They also cache their results so redundant calculations are avoided.

  • They are defined using the reactive() function and
  • Are called like functions (e.g., my_reactive_expression()).
  • When to use this: Use reactive() whenever an expensive computation, data transformation, or filtering operation is shared across multiple outputs (like a plot and a summary table). This prevents duplicate code execution and caches the result until inputs change. Use eventReactive() when you want computation to occur only when a specific action button is clicked rather than instantly on every input tweak.

Example:

server <- function(input, output, session) {
  # A reactive expression that computes a filtered dataset or computation
  squared_val <- reactive({
    input$num * input$num
  })
  
  output$result <- renderText({
    paste0("The square is: ", squared_val())

})
}

An example of eventReactive():

library(shiny)
library(bslib)

ui <- page_fluid(
  titlePanel("EventReactive Demo"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("num", "Choose a number:", min = 1, max = 100, value = 10),
      actionButton("go", "Run Expensive Calculation")
    ),
    mainPanel(
      textOutput("result")
    )
  )
)

server <- function(input, output, session) {
  
  # eventReactive isolates input$num until input$go is clicked
  expensive_result <- eventReactive(input$go, {
    # Simulate a heavy computation or delay
    Sys.sleep(1) 
    input$num * 1000
  })
  
  output$result <- renderText({
    # The output depends on expensive_result(), which only updates when 'go' is clicked
    paste0("Computed result: ", expensive_result())
  })
}

shinyApp(ui = ui, server = server)

Reactive Outputs (render*() functions)

These are the components displayed in the user’s browser, such as plots, tables, or text. They are created using renderPlot(), renderTable(), renderText(), etc., and

  • Automatically re-render when their underlying reactive expressions or inputs change.
  • When to use this: Whenever you want to display visual or textual results back to the user in the UI (e.g. plotOutput, tableOutput, textOutput).

Example:

server <- function(input, output, session) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$num))
  })
}

Observers (observeEvent(), observe())

These are used for side effects, such as updating UI elements, writing to a database, or logging messages, that don’t directly produce an output to be displayed. They execute whenever their dependencies change, similar to reactive expressions,

  • but their primary purpose is to trigger actions rather than return a value.
  • When to use this: Whenever you need to perform an action outside the UI rendering pipeline—such as writing user feedback to a CSV file, printing debug logs to the R console, or triggering updateSelectInput() based on another input’s selection.

Comparing observe() vs observeEvent()

  • observe(): Automatically listens to every reactive value or expression accessed inside its code block. Whenever any of those dependencies change, the observer re-executes.
  • observeEvent(): Explicitly listens only to the specific reactive expression or input passed as the first argument, ignoring other inputs accessed inside the block until the event triggers.

Example 1: Using observe() (fires whenever either input changes)

server <- function(input, output, session) {
  observe({
    # This observer reads both input$name and input$counter.
    # It will re-run automatically whenever EITHER name or counter changes!
    cat("User:", input$name, "clicked count:", input$counter, "\n")
  })
}

Example 2: Using observeEvent() (fires only when the specified action button is clicked)

server <- function(input, output, session) {
  observeEvent(input$save_btn, {
    # Even though we read input$name and input$comment here inside the block,
    # this observer will ONLY execute when input$save_btn is clicked.
    # Typing in name or comment alone will NOT trigger this observer.
    write.csv(
      data.frame(name = input$name, comment = input$comment), 
      "log.csv", 
      append = TRUE
    )
    showNotification("Saved successfully!")
  })
}

Basic Reactivity

The ui is simple because every user gets the same HTML. The server is more complicated because every user needs to get an independent version of the app; when user A moves a slider, user B shouldn’t see their outputs change.

To achieve this independence, Shiny invokes your server() function each time a new session starts.

Inputs are read-only

  • If you try to modify an input directly inside the server function, you’ll get an error.
  • Inputs are read-only from the server side. If you need to programmatically change an input value from the server, use update functions like updateNumericInput() or updateSliderInput().
  • input is selective about who is allowed to read it. To read from an input, you must be in a reactive context created by a function like renderText(), reactive(), or observe(). Trying to access input$num outside of these contexts results in an error.

Example of an invalid read:

server <- function(input, output, session) {
  # Error! Not inside a reactive context
  print(input$num) 
}

Outputs

output, like input, is a list-like object. But output is always used in concert with a render function.

The render function does two things:

  • It sets up a special reactive context that automatically tracks what inputs the output uses.
  • It converts the output of your R code into HTML suitable for display on a web page.

Like the input, the output is picky about how you use it. You’ll get an error if:

  • You forget the render function.
server <- function(input, output, session) {
  output$greeting <- "Hello human"
}
shinyApp(ui, server)
#> Error: Unexpected character object for output$greeting
#> ℹ Did you forget to use a render function?
  • You attempt to read from an output.
server <- function(input, output, session) {
  message("The greeting is ", output$greeting)
}
shinyApp(ui, server)
#> Error: Reading from shinyoutput object is not allowed.

Reactive Programming

Run this complete example to see reactivity in action:

library(shiny)

ui <- fluidPage(
  textInput("name", "What's your name?"),
  textOutput("greeting")
)

server <- function(input, output, session) {
  output$greeting <- renderText({
    paste0("Hello ", input$name, "!")
  })
}

shinyApp(ui, server)

Imperative vs Declarative Programming

This difference between commands and recipes is one of the key differences between two important styles of programming:

  • In imperative programming, you issue a specific command and it’s carried out immediately. This is the style of programming you’re used to in your analysis scripts: you command R to load your data, transform it, visualise it, and save the results to disk.
  • In declarative programming, you express higher-level goals or describe important constraints, and rely on someone else to decide how and/or when to translate that into action. This is the style of programming you use in Shiny.

With imperative code you say Make me a sandwich. With declarative code you say Ensure there is a sandwich in the refrigerator whenever I look inside of it. Imperative code is assertive; declarative code is passive-aggressive.

More at

Mastering Shiny - Basic Reactivity