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 chainsReactive 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 withval(), write withval(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
reactlogto 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
- Reactive Programming Deep Dive
- Shiny Modules for Code Reuse
- Dynamic UI with renderUI and insertUI
- Deploying Shiny Apps