Reactive Objects in Shiny

Last updated on 2024-02-20 | Edit this page

Overview

Questions

  • What is reactivity?
  • How can I avoid re-processing data the same way for multiple outputs?
  • How can I have inputs change in response to other inputs?

Objectives

  • Explain what a reactive context means.
  • Minimize code in our app with reactives.

Our App So Far


We have come far! Here is our app so far. It looks great, but, in reading it, notice that we re-use a whole block of code to filter data. Wouldn’t it be nice to instead only filter that data once, and not duplicate the effort?

R

# 1. Preamble
library(shiny)
library(shinythemes)
library(sf)
library(dplyr)
library(ggplot2)

seagrass_casco <- readRDS("data/joined_seagrass_cover.Rds")

# 2. Define a User Interface
ui <- fluidPage(
  title = "Seagrass in Casco App",
  theme = shinytheme("sandstone"),
  
 titlePanel("Seagrass in Casco Bay over time"),
 
 sidebarLayout(
   
   # sidebar
   sidebarPanel(
     selectInput(
       inputId = "year",
       label = "Choose a year:",
       choices = unique(seagrass_casco$year) |> sort(),
       selected = unique(seagrass_casco$year) |> min() #to get the earliest year
       ),
     
     checkboxGroupInput(
       inputId = "cover",
       label = "Percent Cover Classes:",
       choices = unique(seagrass_casco$cover_pct) |> sort(),
       selected =  unique(seagrass_casco$cover_pct) |> sort()
     ), 
   ),
   
   # main
   mainPanel(
     plotOutput("map"),
     plotOutput("hist"),
   )
 )
)

# 3. define a server
server <- function(input, output) {
  
  # our map block
  output$map <- renderPlot({
    
    dat <- seagrass_casco |>
      filter(year %in% input$year) |>
      filter(cover_pct %in% input$cover)
    
    ggplot() +
      geom_sf(data = dat,
              linewidth = 1.5, 
              color = "darkgreen")
    
  })

  # our histogram block
  output$hist <- renderPlot({

    dat <- seagrass_casco |>
      filter(year %in% input$year) |>
      filter(cover_pct %in% input$cover)
    
    ggplot(data = dat,
           aes(x = hectares)) +
      geom_histogram(bins = 50)
  })

}

shinyApp(ui = ui, server = server)

ERROR

Error: package or namespace load failed for 'sf' in dyn.load(file, DLLpath = DLLpath, ...):
 unable to load shared object '/home/runner/.local/share/renv/cache/v5/R-4.3/x86_64-pc-linux-gnu/units/0.8-5/119d19da480e873f72241ff6962ffd83/units/libs/units.so':
  libudunits2.so.0: cannot open shared object file: No such file or directory

Reactives


At its core, Shiny is all about reactivity Reactivity is when you change the value of x, everything else that relies on x is re-evaluated. In our app, output$map depends on input$year. If you change input$year, output$map changes. This is reactivity in action, and input is a reactive variable.

This is not how things behave normally in R. For example

R

x <- 5
y <- x+10
x <- 10
y

OUTPUT

[1] 15

Note that y is not 20. It is still 15.

Reactive Objects


In Shiny, we can make reactive objects that change in response to inputs inside of our server. For example, in the current app, if our server started like this

R

server <- function(input, output) {
  
 dat <- seagrass_casco |>
      filter(year %in% input$year) |>
      filter(cover_pct %in% input$cover)
    
...

And later on, in one of our render functions, we tried to use dat, Shiny would throw an error. That’s because dat as declared above is a static variable. It does not change with context. Instead, we need to declare it a reactive using reactive(), which works just like render*(). We feed it a code block, and it generates a reactive object.

R

server <- function(input, output) {
  
 dat <- reactive({
   seagrass_casco |>
      filter(year %in% input$year) |>
      filter(cover_pct %in% input$cover)
 }) 
...

We now have a reactive object. To use it, thought, we cannot call dat as before. Instead, we use it like a function with no arguments dat().

R

server <- function(input, output) {
  
 # reactives  
 dat <- reactive({
   seagrass_casco |>
      filter(year %in% input$year) |>
      filter(cover_pct %in% input$cover)
 }) 
   # our map block
  output$map <- renderPlot({

    ggplot() +
      geom_sf(data = dat(),
              linewidth = 1.5, 
              color = "darkgreen")
    
  })

  # our histogram block
  output$hist <- renderPlot({

    ggplot(data = dat(),
           aes(x = hectares)) +
      geom_histogram(bins = 50)
  })
}

Reactive UIs


Now that we can reactively filter our datasets, we can address a second problem. Some years do not contain some cover_pct classes. That means that it’s possible for us to select a value that doesn’t exist. In our current app, this isn’t a complete killer, but, in many other apps, dynamically generating inputs that match a range of values in data can greatly simplify our UI.

This let’s us introduce a new render*() function, renderUI(). This function allows us to create a UI element based on inputs and reactives. We will need to make three changes to our app code.

First, in our server, we need to modify our reactives. As this new UI element will depend on a data frame just filtered to year, we need to split our one reactive into two.

R

server <- function(input, output) {
  
 #reactives  
 dat_year <- reactive({
   seagrass_casco |>
      filter(year %in% input$year) 
 }) 

  dat <- reactive({
   dat_year |>
      filter(cover_pct %in% input$cover)
 }) 
...

Now filtering happens in two steps. The second reactive uses the first. We can still use dat() as before in our other output elements.

Second, we need to add a section to our server to create a UI object with renderUI(). What’s nice is that we can take the old code, paste it in here, and change seagrass_casco to dat_year().

R

output$cover_checks <- renderUI({
  checkboxGroupInput(
    inputId = "cover",
    label = "Percent Cover Classes:",
    choices = unique(dat_year()$cover_pct) |> sort(),
    selected =  unique(dat_year()$cover_pct) |> sort()
  )
  
})

Last, we replace the checkboxGroupInput() in our sidebarPanel() with

R

uiOutput("cover_checks")

Key Points

  • Reactivity is a core to Shiny.
  • To avoid duplicating code, use reactive({}) objects in your server.
  • Use reactive objects to generate dynamic UI elements.