flowchart LR
I["input$dataset"] --> D["dataInput()"]
D --> G["output$graph"]
D --> S["output$summary"]
style I fill:#2b6c8e,color:#fff
style D fill:#f0ad4e,color:#fff
style G fill:#8fd36f,color:#fff
style S fill:#8fd36f,color:#fff
Reactivity and reactive()
Become Paganini: Don’t repeat yourself
The problem: the same code, twice
This is the app of apps/02-datasets: one input, two outputs. Try it:
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 650
## file: ui.R
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput(
inputId = "dataset",
label = "Choose a dataset:",
choices = c("rock", "pressure", "cars")
)
),
mainPanel(
plotOutput("graph"),
verbatimTextOutput("summary")
)
)
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
output$graph <- renderPlot({
data <- switch(input$dataset, # the same code...
rock = rock,
pressure = pressure,
cars = cars)
plot(data[, 1:2])
})
output$summary <- renderPrint({
data <- switch(input$dataset, # ...written twice
rock = rock,
pressure = pressure,
cars = cars)
summary(data[, 1:2])
})
}
Now look at its code:
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
## file: ui.R
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput(
inputId = "dataset",
label = "Choose a dataset:",
choices = c("rock", "pressure", "cars")
)
),
mainPanel(
plotOutput("graph"),
verbatimTextOutput("summary")
)
)
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
output$graph <- renderPlot({
data <- switch(input$dataset, # the same code...
rock = rock,
pressure = pressure,
cars = cars)
plot(data[, 1:2])
})
output$summary <- renderPrint({
data <- switch(input$dataset, # ...written twice
rock = rock,
pressure = pressure,
cars = cars)
summary(data[, 1:2])
})
}
It works, but look at the server: the switch() that picks the dataset is written twice, once in each render*().
- If you add a dataset, you must remember to change it in two places
- Every time the input changes, the same data are prepared twice: with a big file or a slow computation, the app becomes slow
The solution: reactive()
reactive() creates a reactive expression: a recipe that returns a value and that the outputs can share. This is the same app, rewritten with reactive(). From the outside nothing changes:
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 650
## file: ui.R
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput(
inputId = "dataset",
label = "Choose a dataset:",
choices = c("rock", "pressure", "cars")
)
),
mainPanel(
plotOutput("graph"),
verbatimTextOutput("summary")
)
)
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
# written once, used everywhere
dataInput <- reactive({
switch(input$dataset,
rock = rock,
pressure = pressure,
cars = cars)
})
output$graph <- renderPlot({
data <- dataInput() # note the ()
plot(data[, 1:2])
})
output$summary <- renderPrint({
data <- dataInput()
summary(data[, 1:2])
})
}
This is how its server changes:
server <- function(input, output, session) {
1 dataInput <- reactive({
switch(input$dataset,
rock = rock,
pressure = pressure,
cars = cars)
})
output$graph <- renderPlot({
2 data <- dataInput()
plot(data[, 1:2])
})
output$summary <- renderPrint({
data <- dataInput()
summary(data[, 1:2])
})
}- 1
-
Write the recipe once. It reads
input$dataset, so it reacts to it - 2
-
Call it, like a function:
dataInput(), with the parentheses
The complete code, live:
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
## file: ui.R
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput(
inputId = "dataset",
label = "Choose a dataset:",
choices = c("rock", "pressure", "cars")
)
),
mainPanel(
plotOutput("graph"),
verbatimTextOutput("summary")
)
)
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
# written once, used everywhere
dataInput <- reactive({
switch(input$dataset,
rock = rock,
pressure = pressure,
cars = cars)
})
output$graph <- renderPlot({
data <- dataInput() # note the ()
plot(data[, 1:2])
})
output$summary <- renderPrint({
data <- dataInput()
summary(data[, 1:2])
})
}
Now the chain of reactions is:
When input$dataset changes, dataInput() is recomputed once, and both outputs use the new value. If nothing changed, calling dataInput() again just returns the value already computed (Shiny caches it).
reactive()
- Parentheses:
dataInput()is the value,dataInput(without parentheses) is the recipe itself.plot(dataInput[, 1:2])is an error - A reactive expression can only be called inside reactive code (
render*(), anotherreactive()…), exactly likeinput$... - It is lazy: it runs only if some output needs it
Only what depends on a change re-runs
Shiny knows exactly who reads what. In this app each output prints the time it was last computed. Move the slider: which outputs are recomputed? And when you change the dataset?
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 560
## file: ui.R
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
sliderInput("n", "Rows to show:", min = 1, max = 10, value = 3)
),
mainPanel(
h4("Summary (uses only the dataset)"),
verbatimTextOutput("summary"),
h4("First rows (uses the dataset AND the slider)"),
verbatimTextOutput("rows")
)
)
)
## file: server.R
library(shiny)
now <- function() format(Sys.time(), "%H:%M:%OS2")
server <- function(input, output, session) {
dataInput <- reactive({
get(input$dataset)
})
output$summary <- renderPrint({
cat("computed at", now(), "\n\n")
summary(dataInput())
})
output$rows <- renderPrint({
cat("computed at", now(), "\n\n")
head(dataInput(), input$n)
})
}
The code: now() prints the current time, so you can see when each render*() runs.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 560
## file: ui.R
library(shiny)
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
selectInput("dataset", "Choose a dataset:",
choices = c("rock", "pressure", "cars")),
sliderInput("n", "Rows to show:", min = 1, max = 10, value = 3)
),
mainPanel(
h4("Summary (uses only the dataset)"),
verbatimTextOutput("summary"),
h4("First rows (uses the dataset AND the slider)"),
verbatimTextOutput("rows")
)
)
)
## file: server.R
library(shiny)
now <- function() format(Sys.time(), "%H:%M:%OS2")
server <- function(input, output, session) {
dataInput <- reactive({
get(input$dataset)
})
output$summary <- renderPrint({
cat("computed at", now(), "\n\n")
summary(dataInput())
})
output$rows <- renderPrint({
cat("computed at", now(), "\n\n")
head(dataInput(), input$n)
})
}
flowchart LR
I["input$dataset"] --> D["dataInput()"]
D --> S["output$summary"]
D --> R["output$rows"]
N["input$n"] --> R
style I fill:#2b6c8e,color:#fff
style N fill:#2b6c8e,color:#fff
style D fill:#f0ad4e,color:#fff
style S fill:#8fd36f,color:#fff
style R fill:#8fd36f,color:#fff
The slider only touches output$rows; the dataset touches everything that depends on dataInput().
Your turn
In your project (the 02-datasets app), replace the two switch() with a single reactive() called dataInput, and use dataInput() in both outputs.
Then add a third output: a tableOutput("head") showing head(dataInput()).
The server is the one of apps/03-reactive, plus:
output$head <- renderTable({
head(dataInput())
})and in ui.R, inside mainPanel(): tableOutput("head") (remember the comma before it!).