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