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
<- function(input, output) {
server
<- seagrass_casco |>
dat 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
<- function(input, output) {
server
<- reactive({
dat |>
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
<- function(input, output) {
server
#reactives
<- reactive({
dat_year |>
seagrass_casco filter(year %in% input$year)
})
<- reactive({
dat |>
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")