Effective Shiny Programming

Joe Cheng <joe@rstudio.com>

#useR2016 — June 27, 2016

Before we begin

Wifi username: useR2016 Password: Conf@1039

 

We’ll use RStudio Server for this tutorial. To grab an account:

http://user2016.joecheng.com/signup/

(Don’t share user accounts with neighbors—if they sign in with your username, RStudio Server will end your session!)

 

Slides and exercises also available on GitHub:

https://github.com/jcheng5/user2016-tutorial-shiny

Today’s agenda

  • Effective reactive programming
  • Checking preconditions with req()
  • Preview of upcoming features
    • insertUI/removeUI
    • Using databases with Shiny
    • Bookmarkable state

Effective reactive programming

 
 
 
 
 
 
 

We’ll have to keep things moving along to be done on time.

If you have trouble keeping up, you can try again later: Google “shiny conference videos”

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

Just joining us? Go to http://user2016.joecheng.com/signup/ to grab an RStudio Server account, where you’ll find these slides and the exercise files.

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.

Just joining us? Go to http://user2016.joecheng.com/signup/ to grab an RStudio Server account, where you’ll find these slides and the exercise files.

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.

Just joining us? Go to http://user2016.joecheng.com/signup/ to grab an RStudio Server account, where you’ll find these slides and the exercise files.

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!

Just joining us? Go to http://user2016.joecheng.com/signup/ to grab an RStudio Server account, where you’ll find these slides and the exercise files.

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

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

  • eventReactive - observe : observeEvent = reactive : eventReactive
  • isolate - Blocking reactivity
  • reactiveValues - Used in concert with observe/observeEvent, when you can’t model your app logic using the reactive graph
  • 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

Checking preconditions with req()

Sometimes a computation depends on inputs or values that may or may not be available.

  • Needs user to make a choice for which no reasonable default exists
  • Depends on data that must be uploaded by the user
  • Reads an input that doesn’t initially exist as part of the page, but rather, is part of a uiOutput

See req_v1_broken.R

Naive solution: return(NULL)

Check for these conditions (mostly using is.null() or == "") and return(NULL)

This works, but now everyone that reads this reactive must also check for NULL and return early, and so on, layer after layer…

See req_v2_badfix.R

Can we do better?

When we detect an unmet precondition, we don’t really want to return a NULL result; it makes more sense to treat it like an error:

stopifnot(!is.null(input$dataset))

Except that for these cases, we don’t want red error messages in the UI, and we don’t want observers to panic due to uncaught exceptions.

Correct solution: Use req()

req() is like a custom version of stopifnot that:

  1. Doesn’t show an error message in Shiny, and doesn’t cause observers to panic
  2. Performs some common “truthy/falsy” tests to save you some typing (see ?req for details)

See req_v3_goodfix.R

Coming soon

  • Bookmarkable state
  • Database improvements and best practices
  • insertUI/removeUI
  • Modal dialogs and notifications
  • Automatically reconnect on grey-out (opt-in)
  • reactlog now shows elapsed time for reactive expressions and observers
  • Better looking tableOutput

insertUI/removeUI

New functions that complement uiOutput/renderUI.

- `uiOutput` populates a div reactively (`renderUI`); each invocation of the `renderUI` code *replaces* the entire div.
- `insertUI` is used to *add* new UI to the page; nothing is removed or replaced.
- `removeUI` removes specific HTML elements from the page.

Makes some types of apps much easier to code. (“How do I implement an ‘Add plot’ button?”)

Demo: shiny::runApp("insertui.R") (try uploading iris.csv)

Shiny and databases

  • Q: “Can Shiny apps use databases?”
    A: “Uh, obviously… it’s just R…”
  • But there is a difference between using databases interactively, and using them in deployed apps
  • Managing database connections
  • Handling potentially malicious input safely
  • The solution is part code, part public service announcements (see new “Databases” section of shiny.rstudio.com/articles)

SQL injection

query <- paste0(
  "SELECT * FROM table WHERE id = '",
  input$id,
  "';")
dbGetQuery(conn, query)

(It’s just a SELECT query; could go wrong?)

https://xkcd.com/327/

Use interpolation

query <- sqlInterpolate(conn,
  "SELECT * FROM table WHERE id = ?id;",
  id = input$id)
dbGetQuery(conn, query)

If you ever find yourself paste-ing together SQL queries, alarm bells should sound in your head!

By using sqlInterpolate, you’re protected against SQL injection.

(Still better are prepared statements—coming to DBI soon!)

Bookmarkable state

  • Allow users to interact with an app, then “snapshot” the resulting state as a URL that can be bookmarked or shared
  • One of the longest-standing feature requests in Shiny
  • Custom solutions built by many people in many ways over the years, but surprisingly hard to generalize
  • Our solution is informed by prior work by Vincent Nijs (radiant) and Andrzej Oleś (shinyURL)

Bookmarkable state

  • Natively integrated into Shiny—cleaner syntax, better user experience, more robust restore logic
  • Highly configurable
  • Automatic save/restore of inputs; manual save/restore of custom values and/or filesystem data
  • Either encode these values directly in (long/ugly) URL, or persist them on the server and return a short URL with an ID
  • Currently on a branch (wch/shiny@bookmarkable-state) for testing; hopefully landing on master within two or three weeks

Preparing your app

  1. Wrap your UI in function(req):

    ui <- function(req) {
      fluidPage(...)
    }
  2. Add saveStateButton() to UI
  3. In server function, call configureBookmark(...)
  4. If necessary, provide your own callback functions to customize saving/restoring behavior

Bookmarkable state demo

shiny::runApp("bookmark.R")

Deploying

  • For configureBookmark(type="persist"), server support is necessary
  • Shiny Server, Shiny Server Pro, and RStudio Connect support coming this summer
  • ShinyApps.io support TBD (can still use encode mode)