Basic Reactivity

Reactive elements

demos/demo01.R

library(tidyverse)
library(shiny)
library(bslib)

d = read_csv(here::here("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("plot")
)

server = function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(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.

Reactive graph

demos/demo01.R

library(tidyverse)
library(shiny)
library(bslib)

d = read_csv(here::here("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("plot")
)

server = function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(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.

Demo 02 - Adding an input

demos/demo02.R

library(tidyverse)
library(shiny)
library(bslib)

d = readr::read_csv(here::here("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")
)

server = function(input, output, session) {
  output$plot = renderPlot({
    d |>
      filter(name %in% input$name) |>
      ggplot(aes(x = date, y = .data[[input$var]])) +
      geom_line() +
      labs(title = paste(input$name, "-", input$var))
  })
}

shinyApp(ui = ui, server = server)

.data pronoun

One aspect of the updated code that you might not be familiar with is the use of .data[[input$var]] within renderPlot().

This is a 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).

Reactive graph

With these additions, what does our reactive graph look like now?

Your turn - Exercise 03

Starting with the code in exercises/ex03.R (based on demo02.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.

  • You will need to add an appropriate output in the ui
  • and a corresponding reactive expression in the server function to generate these summaries.
  • lubridate::year() will be useful along with dplyr::summarize().
12:00

Reactive graph (again)


reactlog

Another (more detailed) way of seeing the reactive graph (dynamically) for your app is using the reactlog package.

We can run the following to log and show all of the reactive events occuring within ex03_soln.R,


reactlog::reactlog_enable()
(source(here::here("exercises/solutions/ex03_soln.R")))
shiny::reactlogShow()

reactive()

DRY (Don’t repeat yourself)

Some of you may have noticed that in ex03_soln.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/solutions/ex03_soln.R

library(tidyverse)
library(shiny)
library(bslib)

d = readr::read_csv(here::here("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(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      labs(title = paste(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)

Demo 03 - Enter reactive()

demos/demo03.R

library(tidyverse)
library(shiny)
library(bslib)

d = readr::read_csv(here::here("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(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      labs(title = paste(input$name, "-", input$var))
  })
  
  output$minmax = renderTable({
    d_city() |>
      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)

Another reactive expressions

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() tips

  • Expressions are written in the same way as render*() functions but they do not have the output$ prefix.
react_obj = reactive({
  ...
})
  • Any consumer of 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 everyone’s my favorite 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.)

Reactive graph



observers

observer()

These are the final reactive expression we will be discussing today. 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.

  • A side effects in most cases involve sending data to the client browser, e.g. updating a UI element

  • While not obvious given their syntax - the results of the render*() functions are observers.

Demo 04 - Filtering by region

demos/demo04.R

library(tidyverse)
library(shiny)

d = readr::read_csv(here::here("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, "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(aes(x=date, y=.data[[input$var]])) +
      geom_line() +
      labs(title = paste(input$name, "-", input$var))
  })
  
  output$minmax = renderTable({
    d_city() |>
      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 graph

Reactive graph - implicit

Using 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:

# Warning: There were 2 warnings in `summarize()`.
# The first warning was:
# ℹ In argument: `min temp = min(temp_min)`.
# Caused by warning in `min()`:
# ! no non-missing arguments to min; returning Inf
# ℹ Run dplyr::last_dplyr_warnings() to see the 1 remaining warning.

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.

Truthiness in Shiny

In Shiny, “truthiness” determines whether a value should be considered valid for reactive execution.

A value is considered truthy if it is:

  • Not NULL
  • Not FALSE
  • Not an empty vector (character(0), numeric(0), etc.)
  • Not an empty string ""
  • Not NA

isTruthy(TRUE)
[1] TRUE
isTruthy(1)
[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

Your turn - Exercise 04

Using the code provided in exercises/ex04.R (based on demos/demo04.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.

Hint - thinking about how events “flow” through the reactive graph will be helpful here.

10:00

A note on observers

Reactive graphs are meant to be acyclic, that is they should not have circular dependencies.

The use of observers can introduce cycles (accidently) which can then lead to infinite loops, see the following example:

library(shiny)

ui = fluidPage(
  numericInput("n", "n", 0)
)

server = function(input, output, session) {
  observeEvent(input$n, {
    updateNumericInput(inputId = "n", value = input$n + 1)
  })
}

shinyApp(ui=ui, server=server)