Getting Started with Shiny
Reactivity

Colin Rundel

Basic Reactivity

Reactive elements

demos/demo01.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

shinyApp(
  ui = fluidPage(
    titlePanel("Temperature Forecasts"),
    sidebarLayout(
      sidebarPanel(
        radioButtons(
          "city", "Select a city",
          choices = c("Chicago", "Durham", "Sedona", "New York", "Los Angeles")
        ) 
      ),
      mainPanel( 
        plotOutput("plot")
      )
    )
  ),
  server = function(input, output, session) {
    output$plot = renderPlot({
      d |>
        filter(city %in% input$city) |>
        ggplot(aes(x=time, y=temp, color=city)) +
        geom_line()
    })
  }
)

Our inputs and outputs are defined by the elements in our ui.

Reactive expression

demos/demo01.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

shinyApp(
  ui = fluidPage(
    titlePanel("Temperature Forecasts"),
    sidebarLayout(
      sidebarPanel(
        radioButtons(
          "city", "Select a city",
          choices = c("Chicago", "Durham", "Sedona", "New York", "Los Angeles")
        ) 
      ),
      mainPanel( 
        plotOutput("plot")
      )
    )
  ),
  server = function(input, output, session) {
    output$plot = renderPlot({
      d |>
        filter(city %in% input$city) |>
        ggplot(aes(x=time, y=temp, color=city)) +
        geom_line()
    })
  }
)

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)
d = readr::read_csv(here::here("data/weather.csv"))

shinyApp(
  ui = fluidPage(
    titlePanel("Temperature Forecasts"),
    sidebarLayout(
      sidebarPanel(
        radioButtons(
          "city", "Select a city",
          choices = c("Chicago", "Durham", "Sedona", "New York", "Los Angeles")
        ),
        checkboxInput("forecast", "Highlight forecasted data", value = FALSE)
      ),
      mainPanel( plotOutput("plot") )
    )
  ),
  server = function(input, output, session) {
    output$plot = renderPlot({
      
      d_city = filter(d, city %in% input$city)
      
      if (input$forecast) {
        ggplot(d_city, aes(x=time, y=temp, color=source)) +
          geom_line() +
          scale_color_manual(values = c("red","black"))
      } else {
        ggplot(d_city, aes(x=time, y=temp), color=1) +
          geom_line()
      }
    })
  }
)

Reactive graph

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

Your turn - Exercise 03

Start with the code in exercises/ex03.R (based on demo02.R’s code)
Add a tableOutput() to the app’s mainPanel().

Once you have done that, add logic to the server function to render a table that shows the daily min and max temperature for each day of the week.

  • You will need to use renderTable()
  • lubridate::wday() will be useful along with group_by() & summarize()
09:00

Reactive graph (again)



reactlog

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

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

User selected variables

Demo 03 - Not just temperature

demos/demo03.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

d_vars = d |>
  select(where(is.numeric)) |>
  names()

shinyApp(
  ui = fluidPage(
    titlePanel("Weather Forecasts"),
    sidebarLayout(
      sidebarPanel(
        radioButtons(
          "city", "Select a city",
          choices = c("Chicago", "Durham", "Sedona", "New York", "Los Angeles")
        ),
        selectInput(
          "var", "Select a variable",
          choices = d_vars, selected = "temp"
        )
      ),
      mainPanel( 
        plotOutput("plot"),
        tableOutput("minmax")
      )
    )
  ),
  server = function(input, output, session) {
    output$plot = renderPlot({
      d |>
        filter(city %in% input$city) |>
        ggplot(aes(x=time, y=.data[[input$var]])) +
        ggtitle(input$var) +
        geom_line()
    })
    
    output$minmax = renderTable({
      d |>
        filter(city %in% input$city) |>
        mutate(
          day = lubridate::wday(time, label = TRUE, abbr = FALSE),
          date = as.character(lubridate::date(time))
        ) |>
        group_by(date, day) |>
        summarize(
          `min` = min(.data[[input$var]]),
          `max` = max(.data[[input$var]]),
          .groups = "drop"
        )
    })
  }
)

.data & .env

These are an excellent option for avoiding some of the complexity around NSE with rlang (e.g. {{, !!, enquo(), etc.) when working with functions built with the tidy eval framework (e.g. dplyr and ggplot2).

  • .data retrieves data-variables from the data frame.
  • .env retrieves env-variables from the environment.

reactive() & observe()

Don’t repeat yourself (DRY)

demos/demo03.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

d_vars = d |>
  select(where(is.numeric)) |>
  names()

shinyApp(
  ui = fluidPage(
    titlePanel("Weather Forecasts"),
    sidebarLayout(
      sidebarPanel(
        radioButtons(
          "city", "Select a city",
          choices = c("Chicago", "Durham", "Sedona", "New York", "Los Angeles")
        ),
        selectInput(
          "var", "Select a variable",
          choices = d_vars, selected = "temp"
        )
      ),
      mainPanel( 
        plotOutput("plot"),
        tableOutput("minmax")
      )
    )
  ),
  server = function(input, output, session) {
    output$plot = renderPlot({
      d |>
        filter(city %in% input$city) |>
        ggplot(aes(x=time, y=.data[[input$var]])) +
        ggtitle(input$var) +
        geom_line()
    })
    
    output$minmax = renderTable({
      d |>
        filter(city %in% input$city) |>
        mutate(
          day = lubridate::wday(time, label = TRUE, abbr = FALSE),
          date = as.character(lubridate::date(time))
        ) |>
        group_by(date, day) |>
        summarize(
          `min` = min(.data[[input$var]]),
          `max` = max(.data[[input$var]]),
          .groups = "drop"
        )
    })
  }
)

Demo 04 - Using a reactive

demos/demo04.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

d_vars = d |>
  select(where(is.numeric)) |>
  names()

shinyApp(
  ui = fluidPage(
    titlePanel("Weather Forecasts"),
    sidebarLayout(
      sidebarPanel(
        radioButtons(
          "city", "Select a city",
          choices = c("Chicago", "Durham", "Sedona", "New York", "Los Angeles")
        ),
        selectInput(
          "var", "Select a variable",
          choices = d_vars, selected = "temp"
        )
      ),
      mainPanel( 
        plotOutput("plot"),
        tableOutput("minmax")
      )
    )
  ),
  server = function(input, output, session) {
    
    d_city = reactive({
      d |>
        filter(city %in% input$city)
    })
    
    output$plot = renderPlot({
      d_city() |>
        ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
        ggtitle(input$var) +
        geom_line()
    })
    
    output$minmax = renderTable({
      d_city() |>
        mutate(
          day = lubridate::wday(time, label = TRUE, abbr = FALSE),
          date = as.character(lubridate::date(time))
        ) |>
        group_by(date, day) |>
        summarize(
          `min` = min(.data[[input$var]]),
          `max` = max(.data[[input$var]]),
          .groups = "drop"
        )
    })
  }
)

Reactive expressions

These are an example of a “reactive conductor” as they exist in between sources (e.g. an input) and endpoints (e.g. an output).

As such a reactive() depends on various upstream inputs and can be used to generate output.

Their primary use is similar to a function in an R script, they help to

  • avoid repeating yourself

  • decompose complex computations into smaller / more modular steps

  • can improve computational efficiency by breaking up / simplifying reactive dependencies

reactive() tips

  • Code written similarly to render*() functions

  • If react_obj = reactive({...}) then any consumer must access 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 a reactive context (e.g. render*(), reactive(), observer(), etc.)

    ## 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



observer()

These are constructed in the same way as a reactive() however an observer does not return a value, as such they are used for their side effects.

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

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

Demo 05 - Cities AND regions

demos/demo05.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

d_vars = d |>
  select(where(is.numeric)) |>
  names()

shinyApp(
  ui = fluidPage(
    titlePanel("Weather Forecasts"),
    sidebarLayout(
      sidebarPanel(
        selectInput(
          "region", "Select a region",
          choices = sort(unique(d$region))
        ),
        selectInput(
          "city", "Select a city",
          choices = c(),
          multiple = TRUE
        ),
        selectInput(
          "var", "Select a variable",
          choices = d_vars, selected = "temp"
        )
      ),
      mainPanel( 
        plotOutput("plot")
      )
    )
  ),
  server = function(input, output, session) {
    
    d_city = reactive({
      req(input$city)
      d |>
        filter(city %in% input$city)
    })
    
    observe({
      cities = d |>
        filter(region == input$region) |>
        pull(city) |>
        unique() |>
        sort()
      
      updateSelectInput(
        inputId = "city", 
        choices = cities
      )
    })
    
    output$plot = renderPlot({
      d_city() |>
        ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
        ggtitle(input$var) +
        geom_line()
    })
  }
)

Reactive graph



Using req()

You may have notices that the App initializes with East selected for the region but no initial selection for the city. Because of this we have some warnings generated initially:

Warning in min(.data[["temperature"]]) :
  no non-missing arguments to min; returning Inf

This can be a common occurrence, particularly at initialization (or if a user enters bad / unexpected input).

A good way to protect against this is to validate inputs - the simplest way is to use req() which checks if a value is truthy. Non-truthy values prevent further execution of the reactive code (and downstream consumer’s code).

More detailed validation and error reporting is possible with validate() and need().

A cautionary example

library(shiny)

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

Your turn - Exercise 04

Using the code provided in exercise/ex04.R as a starting point add another observer to the app that updates the selectInput() for var such that any variables that are constant (0 variance), for the currently selected cities, are removed.

For example, given this time of year most cities will have 0 risk of snow we would like to exclude the snow variable the var input.

Hint - think about what inputs / reactives would make the most sense to use for this.

09:00

bindEvent()

For both observers and reactive expressions Shiny will automatically determine reactive dependencies for you - in some cases this is not what we want.

To explicitly control the reactive dependencies of reactive expressions, render functions, and observers we can modify them using bindEvent() where the dependencies are explicitly provided.

Similar effects can be achieved via observeEvent() / eventReactive() but these have been soft deprecated as of Shiny 1.6.

Downloading from Shiny

downloadButton() & downloadHandler()

These are the UI and server components needed for downloading a file from your Shiny app. The downloaded file can be of any arbitrary type and content.

downloadButton() is a special case of an actionButton() with specialized server syntax.

Specifically, within the server definition the downloadHandler() is attached to the button’s id via output, e.g.

output$download_btn = downloadHandler(...)

The handler then defines the filename function for generating a default filename and content function for writing the download file’s content to a temporary file, which can then be served by Shiny for downloading.

Demo 06 - A download button

demos/demo06.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))

d_vars = d |>
  select(where(is.numeric)) |>
  names()

shinyApp(
  ui = fluidPage(
    titlePanel("Weather Forecasts"),
    sidebarLayout(
      sidebarPanel(
        selectInput(
          "region", "Select a region",
          choices = sort(unique(d$region))
        ),
        selectInput(
          "city", "Select a city",
          choices = c(),
          multiple = TRUE
        ),
        selectInput(
          "var", "Select a variable",
          choices = d_vars, selected = "temp"
        ),
        downloadButton("download")
      ),
      mainPanel( 
        plotOutput("plot"),
        tableOutput("minmax")
      )
    )
  ),
  server = function(input, output, session) {
    
    output$download = downloadHandler(
      filename = function() {
        paste0(
          paste(input$city,collapse="-") |>
            stringr::str_replace(" ", "_") |>
            tolower(), 
          ".csv"
        )
      },
      content = function(file) {
        readr::write_csv(d_city(), file)
      }
    )
    
    d_city = reactive({
      req(input$city)
      d |>
        filter(city %in% input$city)
    })
    
    observe({
      cities = d |>
        filter(region == input$region) |>
        pull(city) |>
        unique() |>
        sort()
      
      updateSelectInput(
        inputId = "city", 
        choices = cities
      )
    })
    
    output$plot = renderPlot({
      d_city() |>
        ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
        ggtitle(input$var) +
        geom_line()
    })
    
    output$minmax = renderTable({
      d_city() |>
        mutate(
          day = lubridate::wday(time, label = TRUE, abbr = FALSE),
          date = as.character(lubridate::date(time))
        ) |>
        group_by(date, day) |>
        summarize(
          `min` = min(.data[[input$var]]),
          `max` = max(.data[[input$var]]),
          .groups = "drop"
        )
    })
  }
)

Demo 07 - A fancy download button

demos/demo07.R

library(tidyverse)
library(shiny)
d = readr::read_csv(here::here("data/weather.csv"))
d_vars = d |>
  select(where(is.numeric)) |>
  names()

shinyApp(
  ui = fluidPage(
    titlePanel("Weather Forecasts"),
    sidebarLayout(
      sidebarPanel(
        selectInput(
          "region", "Select a region",
          choices = sort(unique(d$region))
        ),
        selectInput(
          "city", "Select a city",
          choices = c(),
          multiple = TRUE
        ),
        selectInput(
          "var", "Select a variable",
          choices = d_vars, selected = "temp"
        ),
        actionButton("download_modal", "Download")
      ),
      mainPanel( 
        plotOutput("plot"),
        tableOutput("minmax")
      )
    )
  ),
  server = function(input, output, session) {
    
    observe({
      showModal(modalDialog(
        title = "Download data",
        checkboxGroupInput(
          "dl_vars", "Select variables to download",
          choices = names(d), selected = names(d), inline = TRUE
        ),
        footer = list(
          downloadButton("download"),
          modalButton("Cancel")
        )
      ))
    }) |>
      bindEvent(input$download_modal)
    
    output$download = downloadHandler(
      filename = function() {
        paste0(
          paste(input$city,collapse="_"), 
          ".csv"
        )
      },
      content = function(file) {
        readr::write_csv(
          d_city() |>
            select(input$dl_vars), 
          file
        )
      }
    )
    
    d_city = reactive({
      req(input$city)
      d |>
        filter(city %in% input$city)
    })
    
    observe({
      cities = d |>
        filter(region == input$region) |>
        pull(city) |>
        unique() |>
        sort()
      
      updateSelectInput(
        inputId = "city", 
        choices = cities
      )
    })
    
    output$plot = renderPlot({
      d_city() |>
        ggplot(aes(x=time, y=.data[[input$var]], color=city)) +
        ggtitle(input$var) +
        geom_line()
    })
    
    output$minmax = renderTable({
      d_city() |>
        mutate(
          day = lubridate::wday(time, label = TRUE, abbr = FALSE),
          date = as.character(lubridate::date(time))
        ) |>
        group_by(date, day) |>
        summarize(
          `min` = min(.data[[input$var]]),
          `max` = max(.data[[input$var]]),
          .groups = "drop"
        )
    })
  }
)

Uploading data

Demo 8 - Using fileInput()

demos/demo08.R

library(tidyverse)
library(shiny)

shinyApp(
  ui = fluidPage(
    titlePanel("Temperature Forecasts"),
    sidebarLayout(
      sidebarPanel(
        fileInput("upload", "Upload a file")
      ),
      mainPanel( 
        h3("Result"),
        tableOutput("result"),
        h3("Content:"),
        tableOutput("data")
      )
    )
  ),
  server = function(input, output, session) {
    output$result = renderTable({
      input$upload
    })
    
    output$data = renderTable({
      req(input$upload)
      ext = tools::file_ext(input$upload$datapath)
      validate(need(ext == "csv", "Please upload a csv file"))
      readr::read_csv(input$upload$datapath)
    })
  }
)

fileInput() widget

This widget behaves a bit differently than the others we have seen - once a file is uploaded it returns a data frame with one row per file and the following columns:

  • name - the original filename (from the client’s system)

  • size - file size in bytes

  • type - file mime type, usually determined by the file extension

  • datapath - location of the temporary file on the server

Given this data frame your app’s server code is responsible for the actual process of reading in the uploaded file.

fileInput() hints

  • input$upload will default to NULL when the app is loaded, using req(input$upload) for downstream consumers is a good idea

  • Files in datapath are temporary and should be treated as ephemeral

    • additional uploads can result in the previous files being deleted
  • type is at best a guess - validate uploaded files and write defensive code

  • The accept argument helps to limit file types but cannot prevent bad uploads

Your turn - Exercise 05

Starting with the code in exercises/ex05.R replace the preloading of the weather data (d and d_vars) with reactive versions that are 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 data/sedona.csv and data/chicago.csv datasets as well.

Hint - remember that anywhere that uses either d or d_vars will now need to use d() and d_vars() instead.

09:00