Reactivity and reactive()

Become Paganini: Don’t repeat yourself

The problem: the same code, twice

This is the app of apps/02-datasets: one input, two outputs. Try it:

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 650
## file: ui.R
library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput(
        inputId = "dataset",
        label = "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      )
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  output$graph <- renderPlot({
    data <- switch(input$dataset,       # the same code...
                   rock = rock,
                   pressure = pressure,
                   cars = cars)
    plot(data[, 1:2])
  })

  output$summary <- renderPrint({
    data <- switch(input$dataset,       # ...written twice
                   rock = rock,
                   pressure = pressure,
                   cars = cars)
    summary(data[, 1:2])
  })
}

Now look at its code:

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
## file: ui.R
library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput(
        inputId = "dataset",
        label = "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      )
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  output$graph <- renderPlot({
    data <- switch(input$dataset,       # the same code...
                   rock = rock,
                   pressure = pressure,
                   cars = cars)
    plot(data[, 1:2])
  })

  output$summary <- renderPrint({
    data <- switch(input$dataset,       # ...written twice
                   rock = rock,
                   pressure = pressure,
                   cars = cars)
    summary(data[, 1:2])
  })
}

It works, but look at the server: the switch() that picks the dataset is written twice, once in each render*().

  • If you add a dataset, you must remember to change it in two places
  • Every time the input changes, the same data are prepared twice: with a big file or a slow computation, the app becomes slow

The solution: reactive()

reactive() creates a reactive expression: a recipe that returns a value and that the outputs can share. This is the same app, rewritten with reactive(). From the outside nothing changes:

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 650
## file: ui.R
library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput(
        inputId = "dataset",
        label = "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      )
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # written once, used everywhere
  dataInput <- reactive({
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  output$graph <- renderPlot({
    data <- dataInput()                 # note the ()
    plot(data[, 1:2])
  })

  output$summary <- renderPrint({
    data <- dataInput()
    summary(data[, 1:2])
  })
}

This is how its server changes:

server <- function(input, output, session) {

1  dataInput <- reactive({
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  output$graph <- renderPlot({
2    data <- dataInput()
    plot(data[, 1:2])
  })

  output$summary <- renderPrint({
    data <- dataInput()
    summary(data[, 1:2])
  })
}
1
Write the recipe once. It reads input$dataset, so it reacts to it
2
Call it, like a function: dataInput(), with the parentheses

The complete code, live:

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
## file: ui.R
library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput(
        inputId = "dataset",
        label = "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      )
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # written once, used everywhere
  dataInput <- reactive({
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  output$graph <- renderPlot({
    data <- dataInput()                 # note the ()
    plot(data[, 1:2])
  })

  output$summary <- renderPrint({
    data <- dataInput()
    summary(data[, 1:2])
  })
}

Now the chain of reactions is:

flowchart LR
    I["input$dataset"] --> D["dataInput()"]
    D --> G["output$graph"]
    D --> S["output$summary"]
    style I fill:#2b6c8e,color:#fff
    style D fill:#f0ad4e,color:#fff
    style G fill:#8fd36f,color:#fff
    style S fill:#8fd36f,color:#fff

When input$dataset changes, dataInput() is recomputed once, and both outputs use the new value. If nothing changed, calling dataInput() again just returns the value already computed (Shiny caches it).

Three things to remember about reactive()
  1. Parentheses: dataInput() is the value, dataInput (without parentheses) is the recipe itself. plot(dataInput[, 1:2]) is an error
  2. A reactive expression can only be called inside reactive code (render*(), another reactive()…), exactly like input$...
  3. It is lazy: it runs only if some output needs it

Only what depends on a change re-runs

Shiny knows exactly who reads what. In this app each output prints the time it was last computed. Move the slider: which outputs are recomputed? And when you change the dataset?

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 560
## file: ui.R
library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput("dataset", "Choose a dataset:",
                  choices = c("rock", "pressure", "cars")),
      sliderInput("n", "Rows to show:", min = 1, max = 10, value = 3)
    ),
    mainPanel(
      h4("Summary (uses only the dataset)"),
      verbatimTextOutput("summary"),
      h4("First rows (uses the dataset AND the slider)"),
      verbatimTextOutput("rows")
    )
  )
)
## file: server.R
library(shiny)

now <- function() format(Sys.time(), "%H:%M:%OS2")

server <- function(input, output, session) {

  dataInput <- reactive({
    get(input$dataset)
  })

  output$summary <- renderPrint({
    cat("computed at", now(), "\n\n")
    summary(dataInput())
  })

  output$rows <- renderPrint({
    cat("computed at", now(), "\n\n")
    head(dataInput(), input$n)
  })
}

The code: now() prints the current time, so you can see when each render*() runs.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 560
## file: ui.R
library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectInput("dataset", "Choose a dataset:",
                  choices = c("rock", "pressure", "cars")),
      sliderInput("n", "Rows to show:", min = 1, max = 10, value = 3)
    ),
    mainPanel(
      h4("Summary (uses only the dataset)"),
      verbatimTextOutput("summary"),
      h4("First rows (uses the dataset AND the slider)"),
      verbatimTextOutput("rows")
    )
  )
)
## file: server.R
library(shiny)

now <- function() format(Sys.time(), "%H:%M:%OS2")

server <- function(input, output, session) {

  dataInput <- reactive({
    get(input$dataset)
  })

  output$summary <- renderPrint({
    cat("computed at", now(), "\n\n")
    summary(dataInput())
  })

  output$rows <- renderPrint({
    cat("computed at", now(), "\n\n")
    head(dataInput(), input$n)
  })
}

flowchart LR
    I["input$dataset"] --> D["dataInput()"]
    D --> S["output$summary"]
    D --> R["output$rows"]
    N["input$n"] --> R
    style I fill:#2b6c8e,color:#fff
    style N fill:#2b6c8e,color:#fff
    style D fill:#f0ad4e,color:#fff
    style S fill:#8fd36f,color:#fff
    style R fill:#8fd36f,color:#fff

The slider only touches output$rows; the dataset touches everything that depends on dataInput().

Your turn

Exercise 1 · In your project

In your project (the 02-datasets app), replace the two switch() with a single reactive() called dataInput, and use dataInput() in both outputs.

Then add a third output: a tableOutput("head") showing head(dataInput()).

The server is the one of apps/03-reactive, plus:

output$head <- renderTable({
  head(dataInput())
})

and in ui.R, inside mainPanel(): tableOutput("head") (remember the comma before it!).