15 Reactive building blocks

Now that you’ve learned how the reactive graph really works, it’s useful to come back to some of the details of the underlying objects. By now, you’ve used these objects many times in your apps but we haven’t yet discussed them as R data structures. We’ll now fix that omission and give you a better sense of how they fit into R the programming language.

There are three fundamental building blocks of reactive programming: reactive values, reactive expressions, and observers. You’ve already seen most of the important parts of reactive values and expressions, so this chapter will spend most time on observers and the closely related outputs. We’ll also discuss a final piece of the invalidation puzzle: timed invalidation.

This chapter will again use the reactive console so that we can experiment with reactivity directly in the console without having to launch a Shiny app each time.

15.1 Reactive values

There are two types of reactive values:

They have slightly different syntaxes for getting and setting values:

x <- reactiveVal(10)
x()       # get
#> [1] 10
x(20)     # set

y <- reactiveValues(a = 1, b = 2)
y$a       # get
#> [1] 1
y$b <- 20 # set

It’s unfortunate that there are two different syntaxes, but there’s no way to standardise them. However, there are no major differences in their behaviour, so you can choose between them based on which syntax you prefer. In this book, I use reactiveVal() because I like that the syntax makes it a little more clear that something unusual is going on.

The most common source of reactive values is the input argument passed to the server function. This is a special read-only reactiveValues(): you’re not allowed to modify the values because Shiny automatically updates them based on the users actions in the browser.

ui <- fluidPage(
  textInput("name", "name")
)
server <- function(input, output, session) {
  input$name <- "Hadley"
}
shinyApp(ui, server)
#> Error: Can't modify read-only reactive value 'Hadley'.

It’s important to note that both types of reactive values have so called reference semantics. Most R objects have copy-on-modify42 semantics which means that if you assign the same value to two names, the connection is broken as soon as you modify one:

a1 <- a2 <- 10
a2 <- 20
a1 # unchanged
#> [1] 10

This is not the case with reactive values — they always keep a reference back to the same value so that modifying any copy modifies all values:

b1 <- b2 <- reactiveVal(10)
b2(20)
b1()
#> [1] 20

15.1.1 Exercises

  1. What are the differences between these two lists of reactive values? Compare the syntax for getting and setting individual reactive values.

    l1 <- reactiveValues(a = 1, b = 2)
    l2 <- list(a = reactiveVal(1), b = reactiveVal(2))
  2. Design and perform a small experiment to verify that reactiveValues() also has reference semantics.

15.2 Reactive expressions

Recall that a reactive has two important properties: it is lazy and cached. This means that it does as little work as possible by only doing any work if it’s actually needed, and when it’s called twice in a row, returning the previous value. There are two important details that we haven’t yet covered: what reactive expressions do with errors, and that you can use on.exit() inside a reactive.

15.2.1 Errors

Reactive expressions cache errors in exactly the same way that they cache values. For example, take this reactive:

r <- reactive(stop("Error occured at ", Sys.time(), call. = FALSE))
r()
#> Error: Error occured at 2021-01-20 23:42:45

If we wait a second or two, we can see that we get the same error as before:

Sys.sleep(2)
r()
#> Error: Error occured at 2021-01-20 23:42:45

Errors are also treated the same way as values when it comes to the reactive graph: errors propagate through the reactive graph exactly the same way as regular values. The only difference is what happens when an error hits an output or observer:

  • An error in an output will be displayed in the app43.
  • An error in an observer will cause the current session to close. (If you don’t want this to happen you can wrap the code in try() or tryCatch().)

This same system powers req(), which emits a special type of error44. The error emitted by req() causes observers and outputs to stop what they’re doing but not otherwise fail. By default, it will cause outputs to reset to their initial blank state, but if you use req(..., cancelOutput = TRUE) they’ll preserve their current display.

15.2.2 on.exit()

You can think of reactive(x()) as a shortcut for function() x(), automatically adding laziness and caching. This means that you can use functions that only work inside functions. The most useful of these is on.exit() which allows you to run code when a reactive expression finishes, regardless of whether the reactive successfully returns an error or fails with an error. You can learn more about on.exit() at https://withr.r-lib.org/articles/changing-and-restoring-state.html.

15.2.3 Exercises

  1. Use the reactlog package to observe an error propagating through the reactives in the following app, confirming that it follows the same rules as value propagation.

    ui <- fluidPage(
      checkboxInput("error", "error?"),
      textOutput("result")
    )
    server <- function(input, output, session) {
      a <- reactive({
        if (input$error) {
          stop("Error!")
        } else {
          1
        }
      })
      b <- reactive(a() + 1)
      c <- reactive(b() + 1)
      output$result <- renderText(c())
    }
  2. Modify the above app to instead use req() instead of stop(). Verify that events still propagate the same way. What happens when you use the cancelOutput argument?

15.3 Observers

An observer sets up a block of code that is run every time one of the reactive values or expressions it uses is updated:

y <- reactiveVal(10)
observe({
  message("`y` is ", y())
})
#> `y` is 10

y(5)
#> `y` is 5
y(4)
#> `y` is 4

Note that the observer runs immediately when you create it — it must do this in order to determine its reactive dependencies.

Observers differ from reactive expressions in two important ways:

  • The value returned by an observer is ignored because they are designed to work with functions called for their side-effects, like cat() or write.csv().

  • Observers are eager and forgetful — they run as soon as they possibly can and they don’t remember their previous action. This eagerness is “infectious” because if they use a reactive expression, that reactive expression will also be evaluated.

15.3.1 Nesting observers

It is useful to think of observer() not as doing something, but as creating something (which then takes action as needed). That mindset helps you to understand what’s going on in this example:

x <- reactiveVal(1)
y <- observe({
  x()
  observe(print(x()))
})
#> [1] 1
x(2)
#> [1] 2
#> [1] 2
x(3)
#> [1] 3
#> [1] 3
#> [1] 3

Every time the observer is triggered, it creates another observer, so each time x changes, its value is printed one more time. As a general rule, you should only ever create observers or outputs at the top-level of your server function. If you find yourself trying to nest them, sit down and sketch out the reactive graph that you’re trying to create — there’s almost certainly a better approach.

It can be harder to spot this mistake directly in a more complex app, but you can always use the reactlog: just look for unexpected churn in observers (or outputs), then track back to what is creating them.

15.3.2 isolate()

Observers are often coupled with reactive values in order to track state changes over time. For example, take this code which tracks how many times x changes:

count <- reactiveVal(0)
x <- reactiveVal(1)
observe({
  x()
  count(count() + 1)
})

If you were to run it, you’d immediately get stuck in an infinite loop because the observer will take a reactive dependency on x and count; and since the observer modifies count, it will immediately re-run.

Fortunately, Shiny provides isolate() to resolve this problem. This function allows you to access the current value of a reactive value or expression without taking a dependency on it:

count <- reactiveVal(0)
x <- reactiveVal(1)
observe({
  x()
  isolate(count(count() + 1))
})

x(1)
x(2)
count()
#> [1] 2
x(3)
count()
#> [1] 3

isolate() is a general tool that also works inside of reactive().

15.3.3 Exercises

  1. Complete the app below with a server function that updates out with the value of x only when the button is pressed.

    ui <- fluidPage(
      numericInput("x", "x", value = 50, min = 0, max = 100),
      actionButton("capture", "capture"),
      textOutput("out")
    )

15.3.4 observeEvent()

Another way to avoid this problem is to use the observeEvent() function that you learned about in Section 4.6. observeEvent(x, y) is equivalent to observe({isolate(x); y}). It decouples listening for an event from the action to handle that event, so you could rewrite the above code as:

observeEvent(x(), {
  count(count() + 1)
})

In Chapter 16 we’ll come back to the problem of managing state over time in much more detail.

observeEvent() has an number of additional arguments that allow you to control its details:

  • By default, observeEvent() will ignore any event that yields NULL (or in the special case of action buttons, 0). Use ignoreNULL = FALSE to also handle NULL values.

  • By default, observeEvent() runs once when you create it. Use ignoreInit = TRUE to skip this run.

  • Use once = TRUE to run the handler only once.

15.3.5 Output

If you’ve paid close attention you might have noticed that I’ve explained input, reactive expressions, and observers, but I haven’t said anything about output. How do reactive outputs fit into the picture? Take this simple output:

output$text <- renderText(paste("Hi ", input$name, "!"))

It looks a bit like a reactive expression because we’re assigning the result of renderText(), but it feels like a observer, because it has a side-effect: updating the contents of a text box in the browser. In fact, outputs are a special type of observer. They have two special properties:

  • They are defined when you assign them into output, i.e. output$text <- ... creates the observer.

  • They have some limited ability to detect when they’re not visible (i.e. they’re in non-active tab) so they don’t have to recompute45.

Outputs are intimately connected to the browser, so there’s no way to experiment with them on the console.

15.4 Timed invalidation

There’s one last building block to discuss. It’s a little different to the others in this chapter because it’s not a type of object, but is instead a special behaviour: timed invalidation. You saw an example of this in Section 4.5.1 with reactiveTimer(), but the time has come to discuss the underlying tool that powers it: invalidateLater().

invalidateLater(ms) causes any reactive consumer to be invalidated in the future, after ms milliseconds. It is useful for creating animations and connecting to data sources outside of Shiny’s reactive framework that may be changing over time. For example, this reactive will automatically generate 10 fresh random normals every half a second46:

x <- reactive({
  invalidateLater(500)
  rnorm(10)
})

And this observer will increment a cumulative sum with a random number:

sum <- reactiveVal(0)
observe({
  invalidateLater(300)
  sum(isolate(sum()) + runif(1))
})

15.4.1 Polling

Take this reactive that re-reads a csv file every second:

data <- reactive({
  on.exit(invalidateLater(1000))
  read.csv("data.csv")
})

This achieves the goal of updating changing data from disk, but it has a serious downside: when you invalidate the reactive, you’re also invalidating all downstream consumers, so even if the data is the same, all the downstream work has to be redone. To avoid this problem, Shiny provides reactivePoll() which takes two functions: one that performs a relatively cheap check to see if the data has changed and another more expensive function that actually does the computation.

We can use reactivePoll() to rewrite the previous reactive as follows. We use file.mtime(), which returns the last time the file was modified, as cheap check to see if we need to reload the file.

server <- function(input, output, session) {
  data <- reactivePoll(1000, session, 
    function() file.mtime("data.csv"),
    function() read.csv("data.csv")
  )
}

Reading a file when it changes is a common task, so Shiny provides an even more specific helper that just needs a file name and a reader function:

server <- function(input, output, session) {
  data <- reactiveFileReader(1000, session, "data.csv", read.csv)
}

15.4.2 Long running reactives

If you’re performing a long running computation, there’s an important question you need to consider: when should you execute invalidateLater()? For example, take this reactive:

x <- reactive({
  invalidateLater(500)
  Sys.sleep(1)
  10
})

Assume Shiny starts the reactive running at time 0, it will request invalidation at time 500. The reactive takes 1000ms to run, so it’s now time 1000, and it’s immediately invalidated and must be recomputed, which then sets up another invalidation: we’re stuck in an infinite loop.

On other hand, if you run invalidateLater() at the end, it will invalidate 500ms after completion, so the reactive will be re-run every 1500 ms.

x <- reactive({
  on.exit(invalidateLater(500), add = TRUE)
  Sys.sleep(1)
  10
})

This is the main reason to prefer invalidateLater() to the simpler reactiveTimer() that we used earlier: it gives you greater control over exactly when the invalidation occurs.

15.4.3 Timer accuracy

The number of milliseconds specified in invalidateLater() is a polite request, not a demand. R may be doing other things when you asked for invalidation to occur, so your request has to wait. This effectively means that the number is a minimum and invalidation might take longer than you expect. In most cases, this doesn’t matter because small differences are unlikely to affect user perception of your app. However, in situations where many small errors will accumulate, you should compute the exact elapsed time and use it to adjust your calculations.

For example, the following code computes distance based on velocity and elapsed time. Rather than assuming invalidateLater(100) always delays by exactly 100 ms, I compute the elapsed time and use it in my calculation of position.

velocity <- 3
distance <- reactiveVal(1)

last <- proc.time()[[3]]
observe({
  cur <- proc.time()[[3]]
  time <- last - cur
  last <<- cur
  
  distance(isolate(distance()) + velocity * time)
  invalidateLater(100)
})

15.4.4 Exercises

  1. Why will this reactive never be executed? Your explanation should talk about the reactive graph and invalidation.

    server <- function(input, output, session) {
      x <- reactive({
        invalidateLater(500)
        rnorm(10)
      })  
    }
  2. If you’re familiar with SQL, use reactivePoll() to only re-read an imaginary “Results” table whenever a new row is added. You can assume the Results table has a timestamp field that contains the date-time that a record was added.

15.5 Summary

In this chapter you’ve learned more about the building blocks that make Shiny work: reactive values, reactive expressions, observers, and timed evaluation. Now we’ll turn our attention to a specific combination of reactive values and observers that allows us to escape some of the constraints (for better and worse) of the reactive graph.