From Shiny I, our app had two inputs (name, var) feeding one output (plot):
Inputs Outputs
------ -------
name --------> plot
var ------/
This note extends the app with a second output, shared data preparation logic, and dynamic UI updates.
reactive() — DRY reactive expressions
The problem: repeated code
As apps grow, it is common to compute the same filtered dataset in multiple render*() calls:
server <-function(input, output, session) { output$plot <-renderPlot({ d |>filter(name %in% input$name) |>ggplot(...) }) output$minmax <-renderTable({ d |>filter(name %in% input$name) |>summarize(...) })}
1
The same filter expression appears in both render functions. When input$name changes, Shiny re-runs both expressions — the same filtering happens twice.
The solution: reactive()
reactive() creates a reactive conductor — an intermediate computation that can be shared across multiple downstream consumers:
reactive({...}) wraps a computation and caches its result. The expression re-executes only when input$name changes.
2
Consumers access the reactive’s value using d_city() — note the parentheses, as if calling a function. Using d_city without parentheses returns the reactive object itself, not its value, which causes the error: Error: object of type 'closure' is not subsettable.
Reactive graph with a conductor
The reactive graph now has an intermediate node:
Inputs Conductors Outputs
------ ---------- -------
name --------> d_city -------> plot
var --------------------/
d_city -------> minmax
d_city is evaluated once when input$name changes; both plot and minmax use the cached result.
reactive() rules
Must be created inside the server() function, not at the top level
May only be read from within other reactive contexts (other reactive(), render*(), or observe() calls)
Always accessed with () — not with the bare name
Is lazily evaluated and cached: it only runs when a downstream consumer requests its value, and returns the cached value until an upstream dependency changes
observe() — reactive side effects
Dynamic UI updates
A reactive conductor returns a value. An observer does not — it is used purely for its side effects. The most common side effect is updating a UI element.
Demo: add a “region” filter that dynamically updates the list of airports in the name input:
The airport choices start empty — they will be populated by the observer.
2
observe({...}) runs whenever any reactive value inside it changes. Here it reads input$region and calls updateSelectInput() to repopulate the "name" dropdown with airports in the selected region.
The updated reactive graph:
Inputs Conductors Outputs / Observers
------ ---------- -------------------
region ---------------------------------> obs (updateSelectInput)
name --------> d_city -------> plot
var --------------------/
d_city -------> minmax
observe() rules
Runs for its side effects, not its return value
Like reactive(), must be inside server() and may only read reactive values from reactive contexts
Runs eagerly: it executes as soon as any dependency changes, even if no output currently needs it
render*() functions are observers — they update a UI output as a side effect
Input validation
req()
Shiny apps execute reactive expressions on initialization, often before the reader has made a selection. This can produce warnings or errors from downstream computations operating on empty or NULL inputs.
req() silently halts execution of a reactive expression if a value is not “truthy”:
d_city <-reactive({req(input$name) d |>filter(name %in% input$name)})
1
If input$name is NULL, "", or an empty vector, req() stops execution silently. The output simply stays blank rather than showing an error.
Truthiness
In Shiny, a value is truthy if it is not NULL, not FALSE, not an empty vector, not "", and not NA:
isTruthy(TRUE)
[1] TRUE
isTruthy("hello")
[1] TRUE
isTruthy(c(1, 2, 3))
[1] TRUE
isTruthy(NULL)
[1] FALSE
isTruthy(FALSE)
[1] FALSE
isTruthy("")
[1] FALSE
isTruthy(character(0))
[1] FALSE
isTruthy(NA)
[1] FALSE
req() vs validate()
req() silently stops execution; validate() stops execution and displays an error message in the UI:
# req() — silent stopd_city <-reactive({req(input$name) d |>filter(name %in% input$name)})# validate() — show error messaged_city <-reactive({validate(need(input$name, "Please select an airport") ) d |>filter(name %in% input$name)})
1
need(condition, message) evaluates the condition; if it is not truthy, the message is displayed in the output area.
Use req() for initialization guards (where silence is appropriate) and validate() when the reader has provided input that fails a logical check and deserves an informative message.
Downloading data from Shiny
downloadButton() and downloadHandler()
downloadButton() is a special UI widget that triggers a file download. Unlike other inputs that return values, it is paired with downloadHandler() in the server:
downloadButton("download") creates the UI button with ID "download".
2
downloadHandler() is assigned to output$download, linking it to the button. It takes two functions: filename and content.
3
content(file) receives a temporary file path and writes the data to it. Shiny sends this file to the browser as a download.
Controlling the reactive graph
bindEvent()
By default, Shiny automatically detects reactive dependencies — any reactive value read inside a render*(), reactive(), or observe() becomes a dependency. To explicitly control when an expression re-executes, use bindEvent():
output$plot <-renderPlot({d_city() |>ggplot(aes(x = date, y = .data[[input$var]])) +geom_line()}) |>bindEvent(input$update_btn)
1
bindEvent(input$update_btn) means the plot only re-renders when the update_btn action button is clicked — not when d_city() or input$var change.
bindEvent() works with reactive(), observe(), and render*() functions. When binding a reactive object, use the functional form: d_city() not d_city.
Avoiding circular reactive graphs
Reactive graphs must be acyclic — no circular dependencies. Observers that update inputs can accidentally create cycles:
server <-function(input, output, session) {observe({updateNumericInput(inputId ="n", value = input$n +1) }) |>bindEvent(input$n)}
1
This observer reads input$n and immediately updates input$n. Each update triggers the observer again — an infinite loop. Use bindEvent() carefully and consider whether an update is truly needed.
File uploads
fileInput()
fileInput() lets readers upload files to the server. Before a file is uploaded, input$<id> returns NULL. After upload, it returns a data frame with one row per file:
accept = ".csv" suggests valid file types in the file picker (not a security guarantee).
2
req(input$upload) prevents execution before any file is uploaded.
3
validate() checks the file extension and shows a message if it is wrong.
4
input$upload$datapath is the server-side path to the uploaded file.
Key points: - datapath points to a temporary location — treat it as ephemeral; new uploads may overwrite previous files - Always validate uploaded file content — the accept argument is a UI hint, not a server-side filter - Anywhere that previously used the static d object must use d() if it becomes a reactive
Note Application Exercise
AE 22: Building a Shiny weather app
Summary
reactive({...}) creates a reactive conductor — a cached, shared computation used by multiple downstream outputs. Always access its value with object(), not object.
observe({...}) is used for side effects only (e.g., updateSelectInput()). It returns no value.
req() silently halts execution if an input is not truthy; validate() / need() halts execution and displays an error message to the reader.