5 Reactive functions

There are a lot of great tutorials that explain the principles behind reactive functions, but that never made any sense to me when I first started out, so I'm just going to give you examples that you can extrapolate principles from.

Reactivity is how Shiny determines which code in server() gets to run when. Some types of objects, such as the input object or objects made by reactiveValues(), can trigger some types of functions to run whenever they change.

For our example, we will use the reactive_demo app. It shows three select inputs that allow the user to choose values from the cut, color, and clarity columns of the diamonds dataset from ggplot2, then draws a plot of the relationship between carat and price for the selected subset.

Figure 5.1: Reactive Demo App. You can also access this app with shinyintro::app("reactive_demo")or view it in a separate tab with the showcase interface.

Here is the relevant code for the UI. There are four inputs: cut, color, clarity, and update. There are two outputs: title and plot.

box(
  title = "Diamonds",
  solidHeader = TRUE,
  selectInput("cut", "Cut", levels(diamonds$cut)),
  selectInput("color", "Color", levels(diamonds$color)),
  selectInput("clarity", "Clarity", levels(diamonds$clarity)),
  actionButton("update", "Update Plot")
),
box(
  title = "Plot",
  solidHeader = TRUE,
  textOutput("title"),
  plotOutput("plot")
)

Whenever an input changes, it will trigger some types of functions to run.

5.1 Render functions

Functions that render an output, like renderText() or renderPlot() will run whenever an input in their code changes. You can trigger a render function just by putting a reactive alone on a line, even if you aren't using it in the rest of the code.

server <- function(input, output, session) {
  output$plot <- renderPlot({
    data <- filter(diamonds,
                   cut == input$cut,
                   color == input$color,
                   clarity == input$clarity)
    
    ggplot(data, aes(carat, price)) +
      geom_point(color = "#605CA8", alpha = 0.5) +
      geom_smooth(method = lm, color = "#605CA8")
  })
  
  output$title <- renderText({
    input$update # just here to trigger the function
    
    sprintf("Cut: %s, Color: %s, Clarity: %s",
                     input$cut,
                     input$color,
                     input$clarity)
  })
} 

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

Which inputs will trigger renderText() to run and produce a new title?

5.2 reactive()

If you move the data filtering outside of renderPlot(), you'll get an error message like "Can't access reactive value 'cut' outside of reactive consumer." This means that the input values can only be read inside certain functions, like reactive(), observeEvent(), or a render function.

However, we can put the data filtering inside reactive(). This means that whenever an input inside that function changes, the code will run and update the value of data(). This can be useful if you need to recalculate the data table each time the inputs change, and then use it in more than one function.

server <- function(input, output, session) {
  data <- reactive({
    filter(diamonds,
           cut == input$cut,
           color == input$color,
           clarity == input$clarity)
  })
  
  title <- reactive({
    sprintf("Cut: %s, Color: %s, Clarity: %s, N: %d",
                     input$cut,
                     input$color,
                     input$clarity)
  })
  
  output$plot <- renderPlot({
    ggplot(data(), aes(carat, price)) +
      geom_point(color = "#605CA8", alpha = 0.5) +
      geom_smooth(method = lm, color = "#605CA8")
  })
  
  output$text <- renderText(title())
} 

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

Which inputs will trigger renderText() to run and produce a new title?

My most common error is trying to use data or title as an object instead of as a function. Notice how the first argument to ggplot is no longer data, but data() and you set the value of data with data(newdata), not data <- newdata. For now, just remember this as a quirk of shiny.

5.3 observeEvent()

What if you only want to update things when the update button is clicked, and not whenever the user changes an option?

We learned about observeEvent() in Section 1.4. This function runs the code whenever the value of the first argument changes. If there are reactive values inside the function, they won't trigger the code to run when they change.

server <- function(input, output, session) {
  observeEvent(input$update, {
    data <- filter(diamonds,
                   cut == input$cut,
                   color == input$color,
                   clarity == input$clarity)
    
    title <- sprintf("Cut: %s, Color: %s, Clarity: %s",
                     input$cut,
                     input$color,
                     input$clarity)
    
    output$plot <- renderPlot({
      ggplot(data, aes(carat, price)) +
        geom_point(color = "#605CA8", alpha = 0.5) +
        geom_smooth(method = lm, color = "#605CA8")
    })
    
    output$title <- renderText(title)
  })
} 

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

In the example above, which inputs will trigger renderText() to run and produce a new title?

You should avoid creating reactive functions inside other functions like I did above. That is because those functions will be triggered by changes to any reactive inputs inside them. It doesn't make a difference in this example because the render functions don't have any reactive values in them, but this can cause huge problems in more complex apps.

5.4 reactiveVal()

You can avoid the problem above of defining a render function inside a reactive function by creating a reactive value using reactiveVal(). This allows you to update the value of data() not just using the code inside the observeEvent() function that created it, but in any function. This is useful when you have multiple functions that need to update that value.

Here, we use observeEvent() to trigger the data filtering code only when the update button is pressed. This new data set is assigned to data() using the code data(newdata).

Because data() returns a reactive value, it will trigger renderPlot() whenever it changes.

server <- function(input, output, session) {
    data <- reactiveVal(diamonds)
    title <- reactiveVal()
    
    observeEvent(input$update, {
        newdata <- filter(diamonds,
                   cut == input$cut,
                   color == input$color,
                   clarity == input$clarity)
    
        newtitle <- sprintf("Cut: %s, Color: %s, Clarity: %s",
                         input$cut,
                         input$color,
                         input$clarity)
            
        data(newdata) # updates data()
        title(newtitle) # updates title()
    })
    
    output$plot <- renderPlot({
        ggplot(data(), aes(carat, price)) +
            geom_point(color = "#605CA8", alpha = 0.5) +
            geom_smooth(method = lm, color = "#605CA8")
    })
    
    output$title <- renderText(title())
} 

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

Which inputs will trigger renderText() to run and produce a new title?

We used data <- reactiveVal(diamonds) in order for data() to have a value that didn't cause an error when renderPlot() runs for the first time.

5.5 reactiveValue()

You need to set up a new reactiveVal() for each value in an app that you want to make reactive. I prefer to use reactiveValues() because it can be used for any new reactive value you need and works just like input, except you can assign new values to it.

You can just set your new object to reactiveValues() or you can initialise it with starting values like below. The object v is a named list, just like input, and when its values change, it triggers reactive functions exactly like input does.

server <- function(input, output, session) {
    v <- reactiveValues(
        data = diamonds,
        title = "All Data"
    )
    
    observeEvent(input$update, {
        v$data <- filter(diamonds,
                       cut == input$cut,
                       color == input$color,
                       clarity == input$clarity)
        
        v$title <- sprintf("Cut: %s, Color: %s, Clarity: %s",
                         input$cut,
                         input$color,
                         input$clarity)
    })
    
    output$plot <- renderPlot({
        ggplot(v$data, aes(carat, price)) +
            geom_point(color = "#605CA8", alpha = 0.5) +
            geom_smooth(method = lm, color = "#605CA8")
    })
    
    output$title <- renderText(v$title)
} 

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

Which inputs will trigger renderText() to run and produce a new title?

Note that you refer to reactive values set up this way as v$data and v$title, not data() and title(), as set them v$data <- newdata, not v$data(newdata).

5.6 eventReactive()

While reactive() is triggered whenever any input values inside it change, eventReactive() is only triggered when the value of the first argument changes, like observeEvent(), but returns a reactive function like reactive().

server <- function(input, output, session) {
  data <- eventReactive(input$update, {
    filter(diamonds,
           cut == input$cut,
           color == input$color,
           clarity == input$clarity)
  })
  
  title <- eventReactive(input$update, {
    sprintf("Cut: %s, Color: %s, Clarity: %s",
                     input$cut,
                     input$color,
                     input$clarity)
  })
  
  output$plot <- renderPlot({
    ggplot(data(), aes(carat, price)) +
      geom_point(color = "#605CA8", alpha = 0.5) +
      geom_smooth(method = lm, color = "#605CA8")
  })
  
  output$text <- renderText(title())
}

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

Which inputs will trigger renderText() to run and produce a new title?

5.7 isolate()

If you want to use an input or reactive value inside a reactive function, but don't want to trigger that function, you can isolate() it. You can also use isolate() to get a reactive value outside a reactive function.

server <- function(input, output, session) {
  data <- reactive({
    filter(
      diamonds,
      cut == isolate(input$cut),
      color == isolate(input$color),
      clarity == input$clarity
    )
  })
  
  title <- reactive({
    sprintf(
      "Cut: %s, Color: %s, Clarity: %s",
      input$cut,
      isolate(input$color),
      isolate(input$clarity)
    )
  })
  
  # what is the title at initialisation?
  debug_msg(isolate(title()))
  
  output$plot <- renderPlot({
    ggplot(data(), aes(carat, price)) +
      geom_point(color = "#605CA8", alpha = 0.5) +
      geom_smooth(method = lm, color = "#605CA8")
  })
  
  output$title <- renderText(title())
} 

In the example above, which inputs will trigger renderPlot() to run and produce a new plot?

Which inputs will trigger renderText() to run and produce a new title?

5.9 Exercises

For the following exercises, clone "reactive_demo" and replace the boxes in the ui with the code below. Delete all the code in server(). Make sure this runs before you go ahead.

box(width = 4,
    selectInput("stat", "Statistic", c("mean", "sd")),
    selectInput("group", "Group By", c("vore", "order", "conservation")),
    actionButton("update", "Update Table")),
box(width = 8,
    solidHeader = TRUE,
    title = textOutput("caption"),
    tableOutput("table"))

You will grouping and summarising the msleep data table from ggplot2 by calculating the mean or standard deviation for all (or some) of the numeric columns grouped by the categorical columns vore, order, or conservation. If you're not sure how to create such a summary table with dplyr, look at the following code for a concrete example.

msleep %>%
  group_by(vore) %>%
  summarise_if(is.numeric, "mean", na.rm = TRUE)
vore sleep_total sleep_rem sleep_cycle awake brainwt bodywt
carni 10.378947 2.290000 0.3733333 13.62632 0.0792556 90.75111
herbi 9.509375 1.366667 0.4180556 14.49062 0.6215975 366.87725
insecti 14.940000 3.525000 0.1611111 9.06000 0.0215500 12.92160
omni 10.925000 1.955556 0.5924242 13.07500 0.1457312 12.71800
NA 10.185714 1.880000 0.1833333 13.81429 0.0076260 0.85800

render

Use render functions to update the output table and caption whenever group or stat change.

server <- function(input, output, session) {
  output$table <- renderTable({
    msleep %>%
      group_by(.data[[input$group]]) %>%
      summarise_if(is.numeric, input$stat, na.rm = TRUE)
  })
  
  output$caption <- renderText({
    sprintf("%ss by %s", toupper(input$stat), input$group)
  })
} 

reactive

Use reactive() to update the output table and caption whenever group or stat change. Ignore the update button.

server <- function(input, output, session) {
  data <- reactive({
    msleep %>%
      group_by(.data[[input$group]]) %>%
      summarise_if(is.numeric, input$stat, na.rm = TRUE)
  })
  output$table <- renderTable(data())
  
  caption <- reactive({
    sprintf("%ss by %s", toupper(input$stat), input$group)
  })
  output$caption <- renderText(caption())
} 

observeEvent

Use observeEvent() to update the output table with the appropriate summary table and to update the caption with an appropriate caption only when the update button is clicked.

server <- function(input, output, session) {
  observeEvent(input$update, {
    data <- msleep %>%
      group_by(.data[[input$group]]) %>%
      summarise_if(is.numeric, input$stat, na.rm = TRUE)
    output$table <- renderTable(data)
    
    caption <-
      sprintf("%ss by %s", toupper(input$stat), input$group)
    output$caption <- renderText(caption)
  })
} 

reactiveVal

Use reactiveVal() to update the output table and caption only when the update button is clicked.

server <- function(input, output, session) {
  data <- reactiveVal()
  caption <- reactiveVal()
  
  observeEvent(input$update, {
    newdata <- msleep %>%
      group_by(.data[[input$group]]) %>%
      summarise_if(is.numeric, input$stat, na.rm = TRUE)
    data(newdata)
    
    # this is an alternative way to set reactiveVal
    # by piping the value into the function
    sprintf("%ss by %s", toupper(input$stat), input$group) %>%
      caption()
  })
  
  output$table <- renderTable(data())
  output$caption <- renderText(caption())
} 

reactiveValues

Use reactiveValues() to update the output table and caption only when the update button is clicked.

server <- function(input, output, session) {
  v <- reactiveValues()
  
  observeEvent(input$update, {
    v$data <- msleep %>%
      group_by(.data[[input$group]]) %>%
      summarise_if(is.numeric, input$stat, na.rm = TRUE)
    
    v$caption <-
      sprintf("%ss by %s", toupper(input$stat), input$group)
  })
  
  output$table <- renderTable(v$data)
  output$caption <- renderText(v$caption)
} 

eventReactive

Use eventReactive() to update the output table and caption only when the update button is clicked.

server <- function(input, output, session) {
  data <- eventReactive(input$update, {
    msleep %>%
      group_by(.data[[input$group]]) %>%
      summarise_if(is.numeric, input$stat, na.rm = TRUE)
  })
  output$table <- renderTable(data())
  
  caption <- eventReactive(input$update, {
    sprintf("%ss by %s", toupper(input$stat), input$group)
  })
  output$caption <- renderText(caption())
} 

5.10 Your App

Add reactive functions to your custom app. Think about which patterns are best for your app. For example, if you need to update a data table when inputs change, and then use it in more than one output, it's best to use reactive() to create a function for the data and callit in the render functions for each output, rather than creating the data table in each render function.