Go to the course GitHub org and find your ae-shiny (repo name will be suffixed with your GitHub name).
Clone the repo in Positron, follow along, and complete the exercises embedded throughout the notes.
NoteLearning objectives
Implement complex reactive features in Shiny
Use reactive() to avoid repeated code
Use observe() for side effects
Validate inputs with req() and validate()
Add download and upload functionality
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:
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.
For example, let’s 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:
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.
Note⌨️ Your turn
Using the code provided in exercises/ex-04.R (based on demos/demo-04.R) as a starting point, add the calls to req() necessary to avoid the initialization warnings that appear when the app loads before any airport is selected.
Think about where in the reactive graph req() is most effective — consider how events “flow” from inputs through conductors to outputs.
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⌨️ Your turn
Starting with the code in exercises/ex-05.R, replace the preloading of the weather data (d <- read_csv(...)) with a reactive() version that is populated via a fileInput() widget.
You should then be able to get the same app behavior as before once data/weather.csv is uploaded. You can also check that your app works with the smaller data/jfk_weather.csv dataset.
Remember that anywhere that uses d will now need to use d() instead.
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.