Chapter 15 Reactive building blocks

Now that you’ve learned how the reactive graph really works, it’s a useful to come to back to some of the details of the underlying objects. By this point, you’ve used these objects many times in your apps, so now it a good time to discuss some of the finer details.

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.

library(shiny)
reactiveConsole(TRUE)

15.1 Reactive values

There are two ways to create reactive values:

  • You can create a single reactive value with reactiveVal().

  • You can create a list of reactive values with reactiveValues()

They have a slightly different syntax 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 given the constraints imposed by R as a programming language, there’s no way standardise them. Fortunately there are no major differences in their behaviour, so you can choose which syntax you prefer. In this book, I prefer reactiveVal() because I like that the syntax makes it more clear that something unusual is going on.

From the outside, a reactiveVal() acts like a function and a reactiveVals() acts like a list, but behind the scenes they track their usage by reactive consumers, so they can automatically invalidate their dependencies.

observeEvent(x(), cat("x is ", x(), "\n", sep = ""))
x(100)
#> x is 100

observeEvent(y$a, cat("y$a is ", y$a, "\n", sep = ""))
y$a <- 100

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

15.2 Reactive expressions

The most important properties of a reactive expression is that it is lazy and cached — it does as little work as possible by only doing any work if it’s actually used, and if it’s called twice in a row, it’ll return the previous result.

The main detail we haven’t covered is what happens when a reactive throws an error. For example, take this reactive that immediately throws an error:

r <- reactive(stop("Error occured at ", Sys.time(), call. = FALSE))
r()
#> Error: Error occured at 2020-10-24 23:14:34

You already know that reactive() caches values. It also caches errors. 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 2020-10-24 23:14:34

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

  • An error in an output will be displayed in the app.
  • 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 error39 with special behaviour. It causes observers and outputs to stop what they’re doing but not otherwise fail. By default, outputs will reset to their initial blank state, but if you use req(..., cancelOutput = TRUE) they’ll preserve their current state.

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())
})
#> Warning: Error in $: object of type 'closure' is not subsettable
#> `y` is 10

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

Observers differ from expressions in two important ways:

  • They are designed to work with functions that change the world in some way, like print(), plot(), or write.csv(). In programming terminology, changing the world is called a side-effect. Unlike pharmaceuticals where side effects are always unintentional and usually negative, we simply mean any effects apart from a function’s return value.

  • They are eager and forgetful — they run as soon as 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 immediately.

15.3.1 Isolate

Observers are often coupled with reactive values in order to track state changes over time. But if you’re not careful, you quickly hit a problem. 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 it modifies count, it will immediately re-run.

Fortunately, Shiny provides a function to resolve this problem: isolate(). 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

Another way to avoid this problem is to use the observeEvent() function that you learned about in Section 4.6. It decouples listening from reaction, so you could rewrite the above 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.

15.3.2 Outputs

If you’ve being paying close attention you might have noticed that I’ve explained input, reactive expressions, and observer, but I haven’t said anything about output. How reactive outputs they fit into the picture? For the purposes of discussion, take this simple output:

output$text <- renderText({
  paste(up_to_x(), collapse = ", ")
})

Is this an observer or a reactive expression? It looks like a reactive expression because we’re assigning the result of renderText(). On the other hand, it feels like a observer, because it has a side-effect: updating the contents of a text box. It turns out that outputs are neither reactive expressions nor observers, and indeed aren’t even a fundamental building block. In fact, they’re a Shiny feature built on top of observers, that have some of the features of a reactive expression.

While observers are eager and reactive expressions are lazy, and outputs are somewhere in between. When an output’s UI element is visible in the browser, outputs execute eagerly; that is, once at startup, and once anytime their relevant inputs or reactive expressions change. However, if the UI element is hidden then Shiny will automatically suspend (pause) that output from reactively executing40.

Outputs also have a somewhat complicated relationship with side-effects, particularly printing and plotting because renderPrint() and renderPlot() capture printing and plotting side-effects and convert them into special values that can be sent to the browser. Apart from these special side-effects handled by matching render functions, outputs should be free of side-effects, as you have no guarantee of when they’ll be performed.

15.3.3 Nesting

It’s important to think of observer() and the render functions not as doing something, but creating something (which then takes action as needed). In other words, you might want to think of observe() as newObserver() and (e.g.) renderText() as newTextRenderer(). 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 trigged, it creates another observer, so each time x changes, one it’s value is printed one more time. The problem is usually a little more subtle in real code, but a typical case is creating an output inside of an observer:

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

Now outputs are only created on assignment, so this doesn’t keep creating new observers, but it will create and delete outputs multiple times, which decreases performance for no gain.

As a general rule, you should only ever create observers or outputs at the top-level of your server function. If you find yourself needing to nest them, sit down and sketch out the reactive graph that you’re trying to create. In this example, we do the computation in a reactive() and then use it in renderPlot():

df <- reactive(head(cars, input$nrows))
output$plot <- renderPlot(plot(df()))

If you make this mistake in a more complex app, you’ll notice that the performance of your app steadily degrades as it duplicates observers or outputs again and again. Or since memory will be steadily sucked up, you might notice that your app runs out of memory after a few hours. In either case, the easiest way to to find the problem is to use the reactlog: just look for the number of observers or outputs growing a lot, then track back to what is creating them.

15.3.4 observeEvent()

Built on top of observe() isolate().

Additional arguments:

  • ignoreNULL — by default, ignores any eventExpr that yield NULL. Use ignoreNULL = FALSE to pass them on.

  • ignoreInit — by default, always runs once when on creation. Use ignoreInit = TRUE to skip this run.

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

15.4 Timed invalidation

There’s one final building blocking that we need to discuss before we continue. It’s a little different to the others we’ve discussed in this chapter as it’s not a type of object, it’s a special behaviour: timed invalidation. You saw an example of this in Section 4.5.1 using reactiveTimer(), but the time has come to discuss the underlying tool that powers it: invalidateLater().

invalidateLater() causes any reactive consumer to be invalidated in the future, after a set number of milliseconds specified by the first argument. 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 second:

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

15.4.1 Polling

This reactive will re-read a csv file every second:

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

But it has a serious downside: when you invalidate the reactive, you’re also invalidating all downstream reactives, so even if the data is same, all the downstream work has to be redone. To avoid this problem Shiny provides reactivePoll() which allows you to specify 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. For a cheap check to see if the file has changed, we use file.mtime() which returns the last time the file was modified.

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 specific helper that just needs a file name and a reading function:

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

15.4.2 Long running reactives

It’s worth considering whether you should run invalidateLater() at the start or end of the block. In other words, is it better to use on.exit() to perform the invalidation, as in the code below?

x <- reactive({
  on.exit(invalidateLater(500), after = TRUE)
  rnorm(10)
})

It doesn’t matter here, but if the contents of the reactive take longer to run, there’s a potential problem that you need to be aware of. For example, take this reactive:

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

It takes 1000ms to run, so it will be invalidated half way through, and then have to immediately run again, getting 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 happens.

15.4.3 Timer accuracy

The number of milliseconds that you specify in invalidateLater() is polite request, not a demand. R may be doing other things when you asked for invalidation, so your code will need to wait. This effectively means that the number is a minimum — Shiny will do its best to run when you asked, but it might have other things going on so your code will run later than you expect.

This generally doesn’t matter because in most cases a few 100ms difference here and there won’t affect the user’s perception of your app. However, for situations where you’re aggregating over time, and a many small errors will add up to a big one, you should record the exact elapsed time so you can adjust. For example, the following code computes distance based on velocity and elapsed time. Rather than assuming invalidateLater(100) causes the code to be run every 0.1s, I actually compute it so that I determine the additional distance more accurately.

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

  1. Technically, a custom condition. See https://adv-r.hadley.nz/conditions.html#custom-conditions for more details.↩︎

  2. In rare cases, you may prefer to process even outputs that aren’t hidden. You can use the outputOptions() function’s suspendWhenHidden to opt out of the automatic suspension feature on an output-by-output basis.↩︎