13 Why reactivity?

13.1 Introduction

The initial impression of Shiny is often that it’s “magic”. Magic is great when you get started because you can make simple apps very very quickly. But magic in software usually leads to disillusionment: without a solid mental model, it’s extremely difficult to predict how the software will act when you venture beyond the borders of its demos and examples. And when things don’t go the way you expect, debugging is almost impossible.

Fortunately, Shiny is “good” magic. As Tom Dale said of his Ember.js JavaScript framework: “We do a lot of magic, but it’s good magic, which means it decomposes into sane primitives.” This is the quality that the Shiny team aspires to for Shiny, especially when it comes to reactive programming. When you peel back the layers of reactive programming, you won’t find a pile of heuristics, special cases, and hacks; instead you’ll find a clever, but ultimately fairly straightforward mechanism. Once you’ve formed an accurate mental model of reactivity, you’ll see that there’s nothing up Shiny’s sleeves: the magic comes from simple concepts combined in consistent ways.

In this chapter, we’ll motivate reactive programming by trying to do without it and then give a brief history of reactivity as it pertains to Shiny.

13.2 Why do we need reactive programming?

Reactive programming is a style of programming that focuses on values that change over time, and calculations and actions that depend on those values. Reactivity is important for Shiny apps because they’re interactive: users change input controls (dragging sliders, typing in textboxes, checking checkboxes, …) which causes logic to run on the server (reading CSVs, subsetting data, fitting models, …) ultimately resulting in outputs updating (plots redrawing, tables updating, …). This is quite different from most R code, which typically deals with fairly static data.

For Shiny apps to be maximally useful, we need reactive expressions and outputs to update if and only if their inputs change. We want outputs to stay in sync with inputs, while ensuring that we never do more work than necessary. To see why reactivity is so helpful here, we’ll take a stab a solving a simple problem without reactivity.

13.2.1 Why can’t you use variables?

In one sense, you already know how to handle “values that change over time”: they’re called “variables”. Variables in R represent values and they can change over time, but they’re not designed to help you when they change. Take this simple example of converting a temperature from Celsius to Fahrenheit:

temp_c <- 10
temp_f <- (temp_c * 9 / 5) + 32
temp_f
#> [1] 50

So far so good: the temp_c variable has the value 10, the temp_f variable has the value 50, and we can change temp_c:

temp_c <- 30

But changing temp_c does not affect temp_f:

temp_f
#> [1] 50

Variables can change over time, but they never change automatically.

13.2.2 What about functions?

You could instead attack this problem with a function:

temp_c <- 10
temp_f <- function() {
  message("Converting") 
  (temp_c * 9 / 5) + 32
}
temp_f()
#> Converting
#> [1] 50

(This is a slightly weird function because it doesn’t have any arguments, instead accessing temp_c from its enclosing environment40, but it’s perfectly valid R code.)

This solves the first problem that reactivity is trying to solve: whenever you access temp_f() you get the latest computation:

temp_c <- -3
temp_f() 
#> Converting
#> [1] 26.6

It doesn’t, however, minimise computation. Every time you call temp_f() it recomputes, even if temp_c hasn’t changed:

temp_f() 
#> Converting
#> [1] 26.6

Computation is cheap in this trivial example, so needlessly repeating it isn’t a big deal, but it’s still unnecessary: if the inputs haven’t changed, why do we need to recompute the output?

13.2.3 Event-driven programming

Since neither variables nor functions work, we need to create something new. In previous decades, we would’ve jumped directly to event-driven programming. Event-driven programming is an appealingly simple paradigm: you register callback functions that will be executed in response to events.

We could implement a very simple event-driven toolkit using R6, as in the example below. Here we define a DynamicValue that has three important methods: get() and set() to access and change the underlying value, and onUpdate() to register code to run whenever the value is modified. If you’re not familiar with R6, don’t worry about the details, and instead focus on following examples.

DynamicValue <- R6::R6Class("DynamicValue", list(
  value = NULL,
  on_update = NULL,

  get = function() self$value,

  set = function(value) {
    self$value <- value
    if (!is.null(self$on_update)) 
      self$on_update(value)
    invisible(self)
  },
  
  onUpdate = function(on_update) {
    self$on_update <- on_update
    invisible(self)
  }
))

So if Shiny had been invented five years earlier, it might have looked more like this, where temp_c uses <<-41 to update temp_f whenever needed.

temp_c <- DynamicValue$new()
temp_c$onUpdate(function(value) {
  message("Converting") 
  temp_f <<- (value * 9 / 5) + 32
})

temp_c$set(10)
#> Converting
temp_f
#> [1] 50

temp_c$set(-3)
#> Converting
temp_f
#> [1] 26.6

Event-driven programming solves the problem of unnecessary computation, but it creates a new problem: you have to carefully track which inputs affect which computations. Before long, you start to trade off correctness (just update everything whenever anything changes) against performance (try to update only the necessary parts, and hope that you didn’t miss any edge cases) because it’s so difficult to do both.

13.2.4 Reactive programming

Reactive programming elegantly solves both problems by combining features of the solutions above. Now we can show you some real Shiny code, using a special Shiny mode, reactiveConsole(TRUE), that makes it possible to experiment with reactivity directly in the console.

As with event-driven programming, we need some way to indicate that we have a special type of variable. In Shiny, we create a reactive value with reactiveVal(). A reactive value has special syntax42 for getting its value (calling it like a zero-argument function) and setting its value (set its value by calling it like a one-argument function).

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

Now we can create a reactive expression that depends on this value:

temp_f <- reactive({
  message("Converting") 
  (temp_c() * 9 / 5) + 32
})
temp_f()
#> Converting
#> [1] 68

As you’ve learned when creating apps, a reactive expression automatically tracks all of its dependencies. So that later, if temp_c changes, temp_f will automatically update:

temp_c(-3)
temp_c(-10)
temp_f()
#> Converting
#> [1] 14

But if temp_c() hasn’t changed, then temp_f() doesn’t need to recompute43, and can just be retrieved from the cache:

temp_f()
#> [1] 14

A reactive expression has two important properties:

  • It’s lazy: it doesn’t do any work until it’s called.

  • It’s cached: it doesn’t do any work the second and subsequent times it’s called because it caches the previous result.

We’ll come back to these important properties in Chapter 14.

13.3 A brief history of reactive programming

If you want to learn more about reactive programming in other languages, a little history might be helpful. You can see the genesis of reactive programming over 40 years ago in VisiCalc, the first spreadsheet:

I imagined a magic blackboard that if you erased one number and wrote a new thing in, all of the other numbers would automatically change, like word processing with numbers.
Dan Bricklin

Spreadsheets are closely related to reactive programming: you declare the relationship between cells using formulas, and when one cell changes, all of its dependencies automatically update. So you’ve probably already done a bunch of reactive programming without knowing it!

While the ideas of reactivity have been around for a long time, it wasn’t until the late 1990s that they were seriously studied in academic computer science. Research in reactive programming was kicked off by FRAN,44 functional reactive animation, a novel system for incorporating changes over time and user input into a functional programming language. This spawned a rich literature,45 but had little impact on the practice of programming.

It wasn’t until the 2010s that reactive programming roared into the programming mainstream through the fast-paced world of JavaScript UI frameworks. Pioneering frameworks like Knockout, Ember, and Meteor (Joe Cheng’s personal inspiration for Shiny) demonstrated that reactive programming could make UI programming dramatically easier. Within a few short years, reactive programming has come to dominate web programming through hugely popular frameworks like React, Vue.js, and Angular, which are all either inherently reactive or designed to work hand-in-hand with reactive back ends.

It’s worth bearing in mind that “reactive programming” is a fairly general term. While all reactive programming libraries, frameworks, and languages are broadly concerned with writing programs that respond to changing values, they vary enormously in their terminology, designs, and implementations. In this book, whenever we refer to “reactive programming”, we are referring specifically to reactive programming as implemented in Shiny. So if you read material about reactive programming that isn’t specifically about Shiny, it’s unlikely that those concepts or even terminology will be relevant to writing Shiny apps. For readers who do have some experience with other reactive programming frameworks, Shiny’s approach is similar to Meteor and MobX, and very different than the ReactiveX family or anything that labels itself Functional Reactive Programming.

13.4 Summary

Now that you understand why reactive programming is needed and have learned a little bit of history, the next chapter will discuss more details of the underlying theory. Most importantly, you’ll solidify your understanding of the reactive graph, which connects reactive values, reactive expressions, and observers, and controls exactly what is run when.