Principles of Reactivity

Joe Cheng <joe@rstudio.com>

#ShinyDevConf — January 30, 2016

Welcome!

Warm up: Side effects

Functions can be executed for one or both of the following reasons:

  1. You want its return value.
  2. You want it to have some other effect.

These are (a bit misleadingly) called side effects. Any effect that is not the return value is a side effect.

Functions with side effects

write.csv(...)

plot(cars)

print(x)

httr.POST(...)

alarm()

More side effects

# Sets a variable in a parent environment
value <<- 10

# Loads into global env by default
source("functions.R")

# Modifies the global search list
library(dplyr)

# Only if foo is an env, ref class, or R6
foo$bar <- TRUE

NOT side effects (when inside a function)

# Modifying *local* variables
value <- 10

# Creating most kinds of objects
list(a = 1, b = 2)

# Data frames are pass-by-value in R so this is OK
dataset <- dataset %>% filter(count > 3)

# Most calculations
a + 1
summary(pressure)
lm(speed ~ dist, data = cars)
predict(wfit, interval = "prediction")

Ehhh… Not side effects

# Reading from disk
readLines("data.csv")

# Making HTTP GET requests
httr.GET("https://api.example.com/data.json")

# Reading global variables
.Random.seed

# Modifying the random seed... ehhhhhh...
runif(10)

If executing your function/expression leaves the state of the world a little different than before it executed, it has side effects.

But if “what happens in func, stays in func” (besides the return value), then it doesn’t have side effects.

Side effect quiz

For each function, write Yes if it has side effects, and No if not.

Question 1

function(a, b) {
  (b - a) / a
}

Question 2

function(values) {
  globalenv()$values <- values
  values
}

Question 3

function() {
  options(digits.secs = 6)
  as.character(Sys.time())
}

Question 4

function(df) {
  df$foo <- factor(df$foo)
  df
}

Question 5

function() {
  readLines("~/data/raw.txt")
}

Question 6

function(values) {
  hist(values, plot = TRUE)
}

Question 7

function() {
  # Create temp file, and delete when function exits
  filePath <- tempfile(fileext = ".png")
  on.exit(file.unlink(filePath))

  # Plot to the temp file as PNG image
  png(filePath); plot(cars); dev.off()

  # Return the contents of the temp file
  readBin(filePath, "raw", n = file.info(filePath)$size)
}

Answers

  1. No
  2. Yes
  3. Yes
  4. No
  5. No
  6. Yes
  7. Mostly no

Side effects make code harder to reason about, since order of execution of different side-effecty functions can matter (in non-obvious ways).

But we still need them. Without side effects, our programs are useless! (If a program executes but has no observable interactions with the world, you may as well have not executed it at all!)

Reactive programming

Ladder of Enlightenment

  1. Made it halfway through the tutorial. Has used output and input.
  2. Made it entirely through the tutorial. Has used reactive expressions (reactive()).
  3. Has used observe() and/or observeEvent(). Has written reactive expressions that depend on other reactive expressions. Has used isolate() properly.
  4. Can say confidently when to use reactive() vs. observe(). Has used invalidateLater.
  5. Writes higher-order reactives (functions that have reactive expressions as input parameters and return values).
  6. Understands that reactive expressions are monads.

Exercise 0

Open Exercise_00.R and complete the server function. Make the plot output show a simple plot of the first nrows rows of a built-in dataset.

You have 3 minutes!

Hint: plot(head(cars, nrows))

Solution

output$plot <- renderPlot({
  plot(head(cars, input$nrows))
})

Anti-solution

observe({
  df <- head(cars, input$nrows)
  output$plot <- renderPlot(plot(df))
})

output$plot1 <- renderPlot(...)

  • DOESN’T mean: “Go update the output "plot1" with the result of this code.”
  • DOES mean: “This code is the recipe that should be used to update the output "plot1".”

Takeaway

Know the difference between telling Shiny to do something, and telling Shiny how to do something.

Reactive expressions

Expressions that are reactive (obviously)

  • Expression: Code that produces a value
  • Reactive: Detects changes in anything reactive it reads
function(input, output, session) {
  # When input$min_size or input$max_size change, large_diamonds
  # will be notified about it.
  large_diamonds <- reactive({
    diamonds %>%
      filter(carat >= input$min_size) %>%
      filter(carat < input$max_size)
  })
  
  # If that happens, large_diamonds will notify output$table.
  output$table <- renderTable({
    large_diamonds() %>% select(carat, price)
  })
  ... continued ...

  # Reactive expressions can use other reactive expressions.
  mean_price <- reactive({
    mean(large_diamonds()$price)
  })
  
  # large_diamonds and mean_price will both notify output$message
  # of changes they detect.
  output$message <- renderText({
    paste0(nrow(large_diamonds()), " diamonds in that range, ",
      "with an average price of $", mean_price())
  })
}
function(input, output, session) {
  
  # This DOESN'T work.
  large_diamonds <- diamonds %>%
    filter(carat >= input$min_size) %>%
    filter(carat < input$max_size)
  
  output$table <- renderTable({
    large_diamonds %>% select(carat, price)
  })
}

large_diamonds would only be calculated once, as the session starts (i.e. as the page first loads in a browser).

Exercise 1

Open up the file Exercise_01.R.

There’s a new tableOutput("table") in ui.R. Have it show the same data frame that is being plotted, using renderTable.

Make sure that the head() operation isn’t performed more than once for each change to input$nrows.

You have 5 minutes.

Solution

function(input, output, session) {

  df <- reactive({
    head(cars, input$nrows)
  })
  
  output$plot <- renderPlot({
    plot(df())
  })
  
  output$table <- renderTable({
    df()
  })
}

Anti-solution 1

function(input, output, session) {

  values <- reactiveValues(df = cars)
  observe({
    values$df <- head(cars, input$nrows)
  })
  
  output$plot <- renderPlot({
    plot(values$df)
  })
  
  output$table <- renderTable({
    values$df
  })
}

Anti-solution 2

function(input, output, session) {

  df <- cars
  observe({
    df <<- head(cars, input$nrows)
  })
  
  output$plot <- renderPlot({
    plot(df)
  })
  
  output$table <- renderTable({
    df
  })
}

Takeaway

Prefer using reactive expressions to model calculations, over using observers to set (reactive) variables.

Exercise 2

Open up the file Exercise_02.R.

This is a working app–you can go ahead and run it. You choose variables from the iris (yawn) data set, and on various tabs it shows information about the selected variables and fits a linear model.

The problem right now, is that each of the four outputs contains copied-and-pasted logic for selecting out your chosen variables, and for building the model. Can you refactor the code so it’s more maintainable and efficient?

You have 5 minutes.

Solution

selected <- reactive({
  iris[, c(input$xcol, input$ycol)]
})

model <- reactive({
  lm(paste(input$ycol, "~", input$xcol), selected())
})

Anti-solution

  # Don't do this!
  
  # Introduce reactive value for each calculated value
  values <- reactiveValues(selected = NULL, model = NULL)
  
  # Use observers to keep the values up-to-date
  observe({
    values$selected <- iris[, c(input$xcol, input$ycol)]
  })
  
  observe({
    values$model <- lm(paste(input$ycol, "~", input$xcol), values$selected)
  })

Takeaway

Seriously, prefer using reactive expressions to model calculations, over using observers to set (reactive) variables.

Observers

Observers are blocks of code that perform actions.

They’re executed in response to changing reactive values/expressions.

They don’t return a value.

observe({
  cat("The value of input$x is now ", input$x, "\n")
})

Observers come in two flavors

  1. Implicit: Depend on all reactive values/expressions encountered during execution.
    observe({...})
     
  2. Explicit: Just depend on specific reactive value/expression; ignore all others. (Also known as “event handler”.)
    observeEvent(eventExpr, {...})
function(input, output, session) {

  # Executes immediately, and repeats whenever input$x changes.
  observe({
    cat("The value of input$x is now ", input$x, "\n")
  })
  
  # Only executes when input$upload_button is pushed. Any reactive
  # values/expressions encountered in the code block are treated
  # as non-reactive values/expressions.
  observeEvent(input$upload_button, {
    httr::POST(server_url, jsonlite::toJSON(dataset()))
  })
}

Exercise 3

Open Exercise_03.R.

Add server logic so that when the input$save button is pressed, the data is saved to a CSV file called "data.csv" in the current directory.

You have 5 minutes!

Solution

# Use observeEvent to tell Shiny what action to take
# when input$save is clicked.
observeEvent(input$save, {
  write.csv(df(), "data.csv")
})

Reactive expressions vs. observers

reactive()

  1. It can be called and returns a value, like a function. Either the last expression, or return().
  2. It’s lazy. It doesn’t execute its code until somebody calls it (even if its reactive dependencies have changed). Also like a function.
  3. It’s cached. The first time it’s called, it executes the code and saves the resulting value. Subsequent calls can skip the execution and just return the value.
  4. It’s reactive. It is notified when its dependencies change. When that happens, it clears its cache and notifies it dependents.
function(input, output, session) {
  reactive({
    # This code will never execute!
    cat("The value of input$x is now ", input$x, "\n")
  })
}
r1 <- function() { runif(1) }
r1()
# [1] 0.8403573
r1()
# [1] 0.4590713
r1()
# [1] 0.9816089
r2 <- reactive({ runif(1) })
r2()
# [1] 0.5327107
r2()
# [1] 0.5327107
r2()
# [1] 0.5327107

The fact that reactive expressions are lazy and cached, is critical.

It’s hard to reason about when reactive expressions will execute their code—or whether they will be executed at all.

All Shiny guarantees is that when you ask a reactive expression for an answer, you get an up-to-date one.

observe() / observeEvent()

  1. It can’t be called and doesn’t return a value. The value of the last expression will be thrown away, as will values passed to return().
  2. It’s eager. When its dependencies change, it executes right away.
  3. (Since it can’t be called and doesn’t have a return value, there’s no notion of caching that applies here.)
  4. It’s reactive. It is notified when its dependencies change, and when that happens it executes (not right at that instant, but ASAP).
reactive() observe()
Callable Not callable
Returns a value No return value
Lazy Eager
Cached N/A
  • reactive() is for calculating values, without side effects.

  • observe() is for performing actions, with side effects.

A calculation is a block of code where you don’t care about whether the code actually executes—you just want the answer. Safe for caching. Use reactive().

An action is where you care very much that the code executes, and there is no answer (return value), only side effects. Use observe()/observeEvent().

(What if you want both an answer AND you want the code to execute? Refactor into two code chunks–separate the calculation from the action.)

reactive() observe()
Purpose Calculations Actions
Side effects? Forbidden Allowed

An easy way to remember

Keep your side effects
Outside of your reactives
Or I will kill you

—Joe Cheng

Takeaway

Use reactive expressions for calculations (no side effects). Use observers for actions (side effects).

Reactive values

A reactiveValues object is like an environment object or a named list: it stores name/value pairs. You get and set values using $ or [[name]].

rv <- reactiveValues(a = 10)

rv$a
# [1] 10

rv$a <- 20

rv[["a"]]
# [1] 20

input is one (read-only) example.

Exercise 4

Open file Exercise_04.R.

Modify the server function so that when the “rnorm” button is clicked, the plot shows a new batch of rnorm(100) values. When “runif” button is clicked, the plot should show a new batch of runif(100).

You have 15 minutes this time.

Solution

function(input, output, session) {
  v <- reactiveValues(data = runif(100))
  
  observeEvent(input$runif, {
    v$data <- runif(100)
  })
  
  observeEvent(input$rnorm, {
    v$data <- rnorm(100)
  })  
  
  output$plot <- renderPlot({
    hist(v$data)
  })
}

Takeaway

When necessary, you can use observers and reactive values together to escape the usual limits of reactivity.

Exercise 5: Challenge!

Exercise_05.R contains a broken application. See if you can figure out how to fix it!

Read the comments in the file for more details.

Takeaways

  • Know the difference between telling Shiny to do something, and telling Shiny how to do something.
  • Prefer using reactive expressions to model calculations, over using observers to set (reactive) variables.
  • Seriously, prefer using reactive expressions to model calculations, over using observers to set (reactive) variables.
  • Use reactive expressions for calculations (no side effects). Use observers for actions (side effects).
  • When necessary, you can use observers and reactive values together to escape the usual limits of reactivity.

Other topics

  • isolate - Blocking reactivity
  • invalidateLater - Time-based reactivity (and so much more)
  • validate and req - Elegant mechanisms for dealing with missing inputs and failed preconditions
  • shinySignals - Higher order reactives by Hadley
  • eventReactive - observe : observeEvent = reactive : eventReactive