
Lecture 22
Cornell University
INFO 3312/5312 - Spring 2026
April 16, 2026
ae-21Instructions
ae-21 (repo name will be suffixed with your GitHub name).renv::restore() to install the required packages, open the Quarto document in the repo, and follow along and complete the exercises.Every Shiny app has a webpage that the user visits,
and behind this webpage there is a computer that serves this webpage by running R (or Python!).
When running your app locally, the computer serving your app is your computer.
When your app is deployed, the computer serving your app is a web server.


Source: Chapter 1 - Dataclysm, by Christian Rudder

Learn more at https://shiny.posit.co/
Modern UI toolkit for Shiny based on Bootstrap
Creation of delightful and customizable Shiny dashboards with cards, value boxes, sidebars, etc.
Use of modern versions of Bootstrap and Bootswatch

Learn more at https://rstudio.github.io/bslib
is now the officially recommended way to build Shiny apps.
camelCasesnake_caseEvery Shiny app has two main components:
A UI (user interface) which controls how the app looks
and a server function which controls how the app works.
# A tibble: 23,376 × 17
id name airport_code country state region longitude latitude date temp_avg temp_min
<dbl> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl> <date> <dbl> <dbl>
1 72202 Miami KMIA US FL South -80.3 25.8 2022-01-01 24.2 21.1
2 72202 Miami KMIA US FL South -80.3 25.8 2022-01-02 24.4 21.7
3 72202 Miami KMIA US FL South -80.3 25.8 2022-01-03 22.7 17.8
4 72202 Miami KMIA US FL South -80.3 25.8 2022-01-04 20.4 16.7
5 72202 Miami KMIA US FL South -80.3 25.8 2022-01-05 22.9 19.4
6 72202 Miami KMIA US FL South -80.3 25.8 2022-01-06 21.4 16.7
7 72202 Miami KMIA US FL South -80.3 25.8 2022-01-07 23.6 20.6
# ℹ 23,369 more rows
# ℹ 6 more variables: temp_max <dbl>, precip <dbl>, snow <dbl>, wind_direction <dbl>,
# wind_speed <dbl>, air_press <dbl>
demos/demo-01.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
ui <- page_sidebar(
title = "Temperatures at Major Airports",
sidebar = sidebar(
radioButtons(
inputId = "name",
label = "Select an airport",
choices = c(
"Hartsfield-Jackson Atlanta",
"Raleigh-Durham",
"Denver",
"Los Angeles",
"John F. Kennedy"
)
)
),
plotOutput(outputId = "plot")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
d |>
filter(name %in% input$name) |>
ggplot(mapping = aes(x = date, y = temp_avg)) +
geom_line()
})
}
shinyApp(ui = ui, server = server)Instructions
Open exercises/ex-01.R and execute it via the Run App button.
Check that you are able to successfully run the Shiny app and are able to interact with it by picking a new airport.
If everything is working, try modifying the code:
radioButtons()weather.csv (i.e. Ithaca)04:00
Shiny uses page layout functions as a way to specify the general organization of the UI of an app.
Our first app used a sidebar layout created with page_sidebar(). This layout consists of:
a title,
a sidebar (which usually holds inputs or extra information),
and then a collection of other Shiny UI elements which form the body of the app (a plot in our case).
This sidebar layout is a good place to start for most simple apps, but there are many other layouts that can be used to create more complex and interesting apps.
Since page_sidebar() uses snake_case we can infer it is from {bslib}. If you’ve seen older Shiny code you may have seen fluidPage() + sidebarLayout() which come from {shiny}.
There are analogues of almost all of Shiny’s original page layouts in {bslib}, either choice is viable and will let you create a working Shiny app.
We have opted to use the {bslib} layouts because they are more modern and typically have a more user friendly (less nested) syntax.
One of the neat tricks of Shiny is that the interface is just a webpage, and this can be seen by the fact that UI functions are just R functions that generate HTML.
We can run any of the following in our console and see the underlying HTML of each element.
<button id="id" type="button" class="btn btn-default action-button">
<span class="action-label">Click me!</span>
</button>
<div class="form-group shiny-input-container">
<label class="control-label" id="test-label" for="test">Test</label>
<div>
<select class="shiny-input-select form-control" id="test"><option value="A" selected>A</option>
<option value="B">B</option>
<option value="C">C</option></select>
</div>
</div>
<img src="shiny.png"/>
Instructions
Starting from the code in exercises/ex-02.R try changing the radioButton() input to one of the following:
selectInput() with multiple = FALSE
selectInput() with multiple = TRUE
checkboxGroupInput()
What happens? Does the app still work as expected?
06:00
demos/demo-01.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
ui <- page_sidebar(
title = "Temperatures at Major Airports",
sidebar = sidebar(
radioButtons(
inputId = "name",
label = "Select an airport",
choices = c(
"Hartsfield-Jackson Atlanta",
"Raleigh-Durham",
"Denver",
"Los Angeles",
"John F. Kennedy"
)
)
),
plotOutput(outputId = "plot")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
d |>
filter(name %in% input$name) |>
ggplot(mapping = aes(x = date, y = temp_avg)) +
geom_line()
})
}
shinyApp(ui = ui, server = server)Our inputs and outputs are defined by the elements in our ui.
demos/demo-01.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
ui <- page_sidebar(
title = "Temperatures at Major Airports",
sidebar = sidebar(
radioButtons(
inputId = "name",
label = "Select an airport",
choices = c(
"Hartsfield-Jackson Atlanta",
"Raleigh-Durham",
"Denver",
"Los Angeles",
"John F. Kennedy"
)
)
),
plotOutput(outputId = "plot")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
d |>
filter(name %in% input$name) |>
ggplot(mapping = aes(x = date, y = temp_avg)) +
geom_line()
})
}
shinyApp(ui = ui, server = server)The reactive logic is defined in our server() function - {shiny} takes care of figuring out what depends on what.
demos/demo-02.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Forecasts",
sidebar = sidebar(
radioButtons(
inputId = "name",
label = "Select an airport",
choices = c(
"Raleigh-Durham",
"Houston Intercontinental",
"Denver",
"Los Angeles",
"John F. Kennedy"
)
),
selectInput(
inputId = "var",
label = "Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
plotOutput(outputId = "plot")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
d |>
filter(name %in% input$name) |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
labs(title = str_c(input$name, "-", input$var))
})
}
shinyApp(ui = ui, server = server).data pronounA useful feature from {rlang} that can be used across much of the {tidyverse} to reference columns in a data frame using a variable.
This helps avoid some of the complexity around “non-standard evaluation” (e.g. {{, !!, enquo(), etc.) when working with functions built with tidyeval (e.g. {dplyr} and {ggplot2}).
More information: .data and .env pronouns
With these additions, what does our reactive graph look like now?
Instructions
Starting with the code in exercises/ex-03.R (based on demo-02.R’s code) add a tableOutput() with the id minmax to the app’s body.
Once you have done that, you should then add logic to the server() function to render the table so that it shows the min and max average temperature for each year contained in the data.
uiserver() function to generate these summaries.lubridate::year() will be useful along with dplyr::summarize().10:00
reactive()Some of you may have noticed that in ex-03-A.R we have a bit of repeated code - specifically the filtering of d to subset the data for the selected airport.
While this is not a big deal here, it can become problematic in more complex apps.
exercises/ex-03-A.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Forecasts",
sidebar = sidebar(
radioButtons(
"name",
"Select an airport",
choices = c(
"Raleigh-Durham",
"Houston Intercontinental",
"Denver",
"Los Angeles",
"John F. Kennedy"
)
),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
plotOutput("plot"),
tableOutput("minmax")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
d |>
filter(name %in% input$name) |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
labs(title = str_c(input$name, "-", input$var))
})
output$minmax <- renderTable({
d |>
filter(name %in% input$name) |>
mutate(
year = lubridate::year(date) |> as.integer()
) |>
summarize(
`min avg temp` = min(temp_min),
`max avg temp` = max(temp_max),
.by = year
)
})
}
shinyApp(ui = ui, server = server)reactive() demos/demo-03.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Forecasts",
sidebar = sidebar(
radioButtons(
"name",
"Select an airport",
choices = c(
"Raleigh-Durham",
"Houston Intercontinental",
"Denver",
"Los Angeles",
"John F. Kennedy"
)
),
selectInput(
"var",
"Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
plotOutput("plot"),
tableOutput("minmax")
)
server <- function(input, output, session) {
d_city <- reactive({
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
labs(title = str_c(input$name, "-", input$var))
})
output$minmax <- renderTable({
d_city() |>
mutate(
year = year(date) |> as.integer()
) |>
summarize(
`min avg temp` = min(temp_min),
`max avg temp` = max(temp_max),
.by = year
)
})
}
shinyApp(ui = ui, server = server)This is an example of a reactive conductor - it is a new type of reactive expression that exists between sources (e.g. an input) and endpoints (e.g. an output).
As such, a reactive() depends on various upstream inputs, returns a value of some kind which is used by 1 or more downstream outputs (or other conductors).
Their primary use is similar to a function in an R script, they help to
Avoid repeating ourselves
Decompose complex computations into smaller / more modular steps
Improve computational efficiency by breaking up / simplifying reactive dependencies
reactive() tipsExpressions are written in the same way as render*() functions but they do not have the output$ prefix.
react_obj must access its value using react_obj() and not react_obj
Think of react_obj as a function that returns the current value
Common cause of the R error
## Error: object of type 'closure' is not subsettable`Like input reactive expressions, may only be used within reactive contexts
## Error: Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive expression or observer.)observer()These are the final reactive expression we will be discussing. They are constructed in the same way as a reactive() however an observer does not return a value, instead they are used for their side effects.
render*() functions are observers. demos/demo-04.R
library(tidyverse)
library(shiny)
library(bslib)
d <- read_csv(here::here("ae/ae-21-weather/data/weather.csv"))
d_vars <- c(
"Average temp" = "temp_avg",
"Min temp" = "temp_min",
"Max temp" = "temp_max",
"Total precip" = "precip",
"Snow depth" = "snow",
"Wind direction" = "wind_direction",
"Wind speed" = "wind_speed",
"Air pressure" = "air_press"
)
ui <- page_sidebar(
title = "Weather Forecasts",
sidebar = sidebar(
selectInput(
"region",
label = "Select a region",
choices = c("West", "Midwest", "Northeast", "South")
),
selectInput(
"name",
label = "Select an airport",
choices = c()
),
selectInput(
"var",
label = "Select a variable",
choices = d_vars,
selected = "temp_avg"
)
),
plotOutput("plot"),
tableOutput("minmax")
)
server <- function(input, output, session) {
observe({
updateSelectInput(
session = session,
inputId = "name",
choices = d |>
distinct(region, name) |>
filter(region == input$region) |>
pull(name)
)
})
d_city <- reactive({
d |>
filter(name %in% input$name)
})
output$plot <- renderPlot({
d_city() |>
ggplot(mapping = aes(x = date, y = .data[[input$var]])) +
geom_line() +
labs(title = str_c(input$name, "-", input$var))
})
output$minmax <- renderTable({
d_city() |>
mutate(
year = year(date) |> as.integer()
) |>
summarize(
`min avg temp` = min(temp_min),
`max avg temp` = max(temp_max),
.by = year
)
})
}
shinyApp(ui = ui, server = server)

req()You may have noticed that the App initializes with “West” selected for region but no initial selection for name. Because of this we have some warnings generated in the console:
This is a common occurrence with Shiny, particularly at initialization or when a user enters partial / bad input(s).
A good way to protect against this is to validate inputs before using them - the simplest way is to use req() which checks if a value is truthy and prevent further execution if not.
In Shiny, “truthiness” determines whether a value should be considered valid for reactive execution.
A value is considered truthy if it is:
NULLFALSEcharacter(0), numeric(0), etc.)""NA
Instructions
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.
Also, think about if there are any other locations in our app where req() might be useful.
Tip
Thinking about how events “flow” through the reactive graph will be helpful here.
10:00
Reactive graphs are meant to be acyclic, that is they should not have circular dependencies.
The use of observers can introduce cycles (accidentally) which can then lead to infinite loops, see the following example:
From Mastering Shiny
reactive() and observers is essential for building effective Shiny apps