5 Reactivity

Optimizing Shiny apps is important for returning results to individual users quickly, and for serving multiple users simultaneously. With this in mind, review the code for our example app in the last chapter. We filtered the mtcars dataset twice and fit a linear model and extracted the coefficients twice, once each inside each of the render*() functions.

A reasonable idea to make our code more efficient is to do each of these only once and store the results in objects we can call dat and coefs.

The code below attempts to do that. Copy the code and run the app.

library(shiny)
library(dplyr)
library(ggplot2)

ui <-
  fluidPage(
    checkboxGroupInput(inputId = "cyls",
                       label = "Number of cylinders:",
                       choices = c(4, 6, 8),
                       selected = c(4, 6, 8)),
    
    plotOutput(outputId = "myPlot"),
    
    textOutput(outputId = "myText")
  )

server <-
  function(input, output) {
    
    dat <- mtcars |> filter(cyl %in% input$cyls)
    coefs <- lm(mpg ~ wt, data = dat) |> coef()
    
    output$myPlot <-
      renderPlot({
        ggplot(dat, aes(wt, mpg)) +
          geom_point() +
          geom_abline(intercept = coefs[1], slope = coefs[2], linetype = 2)
      })
    
    output$myText <-
      renderText({
        paste("The line has intercept", 
              round(coefs[1], 2), 
              "and slope", 
              round(coefs[2], 2)
              )      
        })
    
  }

shinyApp(ui = ui, server = server)

The app fails, and the error message is surprisingly informative:

x Can't access reactive value 'cyls' outside of reactive consumer.
i Do you need to wrap inside reactive() or observer()?

The code we write inside of render*() will run whenever input is updated. Outside of render*(), the code will not update, and we cannot access values stored in input.

We need to wrap the expressions for both dat and coefs in reactive({}). dat is reactive because it accesses the object in input$cyls, and coefs is reactive because it calls dat.

To call dat and coefs, we need to suffix them with () since reactive values are technically functions. See in the code below how coefs references dat in data = dat(), and the ggplot() call specifies dat() as the dataset.

coefs is a vector of length two, so referencing a specific element is done in the form coefs()[1] or coefs()[2].

The updated server function is below. Copy it into the script you made for the code above to see it work.

server <-
  function(input, output) {
    
    dat <-
      reactive ({
        mtcars |> filter(cyl %in% input$cyls)
      })
    
    coefs <-
      reactive({
        lm(mpg ~ wt, data = dat()) |> coef()
      })
    
    output$myPlot <-
      renderPlot({
        ggplot(dat(), aes(wt, mpg)) +
          geom_point() +
          geom_abline(intercept = coefs()[1], slope = coefs()[2], linetype = 2)
      })
    
    output$myText <-
      renderText({
        paste("The line has intercept", 
              round(coefs()[1], 2), 
              "and slope", 
              round(coefs()[2], 2)
              )      
        })
    
  }

5.1 Exercises

Check Your Understanding:

Copy and paste the following code into a new R script and save it. Click “Run App”. The app should run properly, but not efficiently because values and blocks of code are repeated multiple times. Rewrite server to use at least one reactive value, such as the filtered dataset, or the number of rows, each of which the app currently creates more than once.

library(shiny)
library(dplyr)
library(ggplot2)

ui <- 
  fluidPage(
    sliderInput(inputId = "hp_range", 
                label = "Pick a range for horsepower (hp)", 
                min = 52,
                max = 335,
                value = c(52, 335)),
    
    plotOutput(outputId = "myPlot"),
    
    textOutput(outputId = "myText"),
    
    tableOutput(outputId = "myTable")
  )

server <-
  function(input, output) {
    
    output$myPlot <-
      renderPlot({
        mtcars |> 
          filter(
            between(hp, 
                    input$hp_range[1], 
                    input$hp_range[2])
          ) |> 
        ggplot(aes(wt, mpg)) + 
          geom_point(aes(color = as.factor(cyl))) + 
          geom_smooth(se = F) +
          labs(caption = paste(nrow(filter(mtcars, 
                                           between(hp, 
                                                   input$hp_range[1], 
                                                   input$hp_range[2])
                                           )
                                    ),
                               "cases from the mtcars dataset"
                               )
               )
      })
    
    output$myText <-
      renderText({
        dat <-
          mtcars |>
          filter(
            between(hp, 
                    input$hp_range[1], 
                    input$hp_range[2])
          )
        
        paste(nrow(dat), 
              "cases in the mtcars dataset have values of horsepower between",
              input$hp_range[1], 
              "and", 
              input$hp_range[2])
      })
    
    output$myTable <-
      renderTable({
        mtcars |> 
          filter(
            between(hp, 
                    input$hp_range[1], 
                    input$hp_range[2])
          )
      })
  }

shinyApp(ui = ui, server = server)

Create Your Own App:

  1. Find where your app repeats processes, and use reactive values instead. If your code does not reuse any objects, add another output, such as a table or text, so that it does.