0Pricing
R Academy · Lesson

Reactive Programming Deep Dive

Understand reactive values, reactives, observers, and the reactive graph.

Reactive Programming Deep Dive is a free R Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Reactivity: The Core Idea

Shiny's reactive programming model automatically tracks dependencies between computations. When an input changes, only the outputs that depend on it re-execute — nothing more. Understanding this dependency graph is the key to writing efficient, bug-free Shiny apps.

library(shiny)

# Simplest reactive relationship:
# output depends on input — Shiny tracks this automatically
server <- function(input, output, session) {
  output$result <- renderText({
    paste('You typed:', input$text_in)  # depends on text_in
  })
}

reactive({}) — Shared Computations

reactive({...}) creates a reactive expression whose result is cached and shared. If multiple outputs use the same expensive computation, wrap it in reactive() to compute it once per change rather than once per consumer. Call it like a function: data_subset().

server <- function(input, output, session) {
  # Computed once, shared by multiple outputs
  filtered_data <- reactive({
    subset(mtcars, cyl == input$cyl_select)
  })

  output$plot  <- renderPlot({ plot(filtered_data()$wt,
                                    filtered_data()$mpg) })
  output$table <- renderTable({ filtered_data() })
  # filtered_data() is only recomputed once when cyl_select changes
}

reactiveVal() — Single Reactive Value

reactiveVal(initial_value) creates a reactive variable that you can read and write programmatically. Read by calling it with no arguments; write by calling it with a new value. This is useful for storing mutable state that multiple observers or outputs depend on.

server <- function(input, output, session) {
  counter <- reactiveVal(0)   # initial value = 0

  observeEvent(input$increment, {
    counter(counter() + 1)    # read current, write new
  })

  observeEvent(input$reset, {
    counter(0)                 # reset to 0
  })

  output$count <- renderText(paste('Count:', counter()))
}

reactiveValues() — Multiple State Variables

reactiveValues(key = value, ...) is like a named list where each element is reactive. When any element changes, only the outputs reading that element invalidate. Use it to group related state variables that are updated together.

server <- function(input, output, session) {
  state <- reactiveValues(
    data         = NULL,
    current_page = 1,
    total_rows   = 0
  )

  observeEvent(input$load_btn, {
    state$data       <- read.csv(input$file$datapath)
    state$total_rows <- nrow(state$data)
    state$current_page <- 1
  })

  output$info <- renderText({
    paste('Page', state$current_page, '| Rows:', state$total_rows)
  })
}

observeEvent() — Respond to Events

observeEvent(eventExpr, handlerExpr) runs handlerExpr whenever eventExpr changes. It is used for side effects: updating state, writing to a database, downloading a file, or navigating to a tab. The handler does not return a value.

server <- function(input, output, session) {
  observeEvent(input$save_btn, {
    # Runs once each time save_btn is clicked
    write.csv(current_data(), '/tmp/export.csv', row.names = FALSE)
    showNotification('Saved!', type = 'message')
  })

  observeEvent(input$reset_btn, {
    updateTextInput(session, 'search', value = '')
    updateSliderInput(session, 'range', value = c(0, 100))
  })
}

eventReactive() — Value on Demand

eventReactive(eventExpr, valueExpr) is like reactive() but only recomputes when a specific event fires (e.g. a button click), not every time its dependencies change. Use it to run expensive computations only on explicit user request.

server <- function(input, output, session) {
  # Only re-run the model when 'Run Model' is clicked
  model_result <- eventReactive(input$run_btn, {
    # Expensive computation — only on button click
    lm(as.formula(input$formula), data = get(input$dataset))
  })

  output$summary <- renderPrint({
    summary(model_result())
  })
}

isolate() — Read Without Dependency

isolate({expr}) reads a reactive value or expression without establishing a reactive dependency. The surrounding computation will NOT re-run when the isolated value changes. Use it inside observe() or observeEvent() to access current state without subscribing to changes.

server <- function(input, output, session) {
  observeEvent(input$add_row_btn, {
    # Read current_data without depending on it
    current <- isolate(data_rv())
    new_row <- data.frame(
      id    = nrow(current) + 1,
      value = input$new_value
    )
    data_rv(rbind(current, new_row))
  })
}

invalidateLater() — Auto-Refresh

invalidateLater(ms) inside a reactive context schedules it to re-execute after ms milliseconds. This is used for polling: refreshing a live data feed, updating a clock, or checking for new database rows at regular intervals.

server <- function(input, output, session) {
  live_data <- reactive({
    invalidateLater(5000)   # re-run every 5 seconds
    # Fetch fresh data from external source
    httr2::request('https://api.example.com/stats') |>
      httr2::req_perform() |>
      httr2::resp_body_json()
  })

  output$live_plot <- renderPlot({
    plot(live_data()$time, live_data()$value, type = 'l')
  })
}

reactlog — Debug the Reactive Graph

The reactlog package visualises the reactive dependency graph. Enable it before running the app and call reactlog_show() after interacting to see which reactive nodes invalidated and in what order. Essential for debugging complex apps.

library(reactlog)

# Enable reactlog BEFORE launching the app
reactlog_enable()

# Launch your app
shinyApp(ui, server)

# After interacting in the browser, in the R console:
shiny::reactlogShow()

# A viewer opens showing the reactive graph
# with timestamps and invalidation chains

Reactive Isolation Anti-Pattern

A common mistake: accidentally reading a reactive value inside a non-reactive context, or creating unwanted dependencies by reading a reactive inside a computation that should be isolated. Always be intentional about where you create dependencies versus where you read state as a one-time snapshot.

server <- function(input, output, session) {
  # WRONG: output depends on input$name, but also re-runs
  # every time input$slider changes (unintended dependency)
  output$msg <- renderText({
    paste(input$name, 'total:', input$slider * 2)
  })

  # CORRECT: output only responds to input$name changes;
  # slider is read as a snapshot
  output$msg_correct <- renderText({
    slider_val <- isolate(input$slider)
    paste(input$name, 'snapshot total:', slider_val * 2)
  })
}

observe() vs observeEvent()

observe({...}) creates a reactive observer that re-runs automatically whenever any reactive it reads changes. observeEvent(event, {...}) is more controlled: it only triggers on a specific event. Prefer observeEvent() for button-driven side effects to avoid unintended re-execution.

server <- function(input, output, session) {
  # observe: re-runs on ANY change in input$x or input$y
  observe({
    cat('x or y changed:', input$x, input$y, '\n')
  })

  # observeEvent: only runs when button is clicked
  observeEvent(input$submit_btn, {
    cat('Form submitted with x =', isolate(input$x), '\n')
  }, ignoreNULL = TRUE, ignoreInit = TRUE)
}

Quick Check

What is the key difference between reactive({}) and eventReactive(event, {})?

Reactive Programming Recap

Key takeaways from Reactive Programming Deep Dive:

  • reactive({}): cached computation, re-runs when dependencies change.
  • reactiveVal(init): single mutable reactive variable; read with val(), write with val(new).
  • reactiveValues(...): named list of reactive state variables.
  • observeEvent(event, {}): side effects triggered by a specific event.
  • eventReactive(event, {}): value computed only when a specific event fires.
  • isolate({}): read a reactive without creating a dependency.
  • invalidateLater(ms): schedule periodic re-execution for live updates.
  • Use reactlog to visualise and debug the reactive dependency graph.
server <- function(input, output, session) {
  # State
  rv <- reactiveValues(data = NULL, n = 0)

  # Load data on button click
  observeEvent(input$load, {
    rv$data <- read.csv(input$file$datapath)
    rv$n    <- nrow(rv$data)
  })

  # Expensive model only on 'Run' click
  model <- eventReactive(input$run, {
    lm(y ~ ., data = isolate(rv$data))
  })

  output$summary <- renderPrint({ summary(model()) })
}

Frequently asked questions

Is the “Reactive Programming Deep Dive” lesson free?

Yes — the full text of “Reactive Programming Deep Dive” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.

What will I learn in “Reactive Programming Deep Dive”?

Understand reactive values, reactives, observers, and the reactive graph. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Reactive Programming Deep Dive” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Reactive Programming Deep Dive
  2. Shiny Modules for Code Reuse
  3. Dynamic UI with renderUI and insertUI
  4. Deploying Shiny Apps
← Back to R Academy