2023-08-03
There are three fundamental building blocks of reactive programming:
Most of the reactive values you’ll encounter will come from the input
argument to the server
function:
reactiveValues
and observeEvent
library(shiny)
ui <- fluidPage(
actionButton("drink", "drink me"),
actionButton("eat", "eat me"),
textOutput("notice")
)
server <- function(input, output, session) {
r <- reactiveValues(notice = "")
observeEvent(input$drink, {
r$notice <- "You are no longer thristy"
})
observeEvent(input$eat, {
r$notice <- "You are no longer hungry"
})
output$notice <- renderText(r$notice)
}
shinyApp(ui, server)
library(shiny)
library(ggplot2)
library(dplyr)
library(shinycssloaders)
# a sample data frame
data <- data.frame(
"category" = c('A', 'B', 'C', 'D'),
"amount" = c(21, 46, 27, 6)
)
ui <- fluidPage(
fluidRow(
column(
width = 2,
actionButton(
"bar",
"",
icon = icon("chart-bar")
),
actionButton(
"pie",
"",
icon = icon("chart-pie")
),
),
column(
width = 10,
plotOutput("p1")
)
)
)
server <- function(input, output, session) {
# complete the server part
}
shinyApp(ui, server)
library(shiny)
library(shinycssloaders)
library(dplyr)
library(ggplot2)
# a sample data frame
data <- data.frame(
"category" = c("A", "B", "C", "D"),
"amount" = c(21, 46, 27, 6)
)
ui <- fluidPage(
fluidRow(
column(
width = 2,
actionButton(
"bar",
"",
icon = icon("chart-bar")
),
actionButton(
"pie",
"",
icon = icon("chart-pie")
),
),
column(
width = 10,
plotOutput("p1") %>%
withSpinner()
)
)
)
server <- function(input, output, session) {
rv <- reactiveValues(
plot = NULL
)
# bar chart
observeEvent(input$bar, {
rv$plot <- ggplot(data, aes(x = category, y = amount, fill = category)) +
geom_bar(stat = "identity")
})
# pie chart
observeEvent(input$pie, {
rv$plot <- ggplot(data, aes(x = "", y = amount, fill = category)) +
geom_bar(stat = "identity", width = 1) +
coord_polar("y", start = 0)
})
output$p1 <- renderPlot({
rv$plot
})
}
shinyApp(ui, server)
library(shiny)
ui <- fillPage(
plotOutput("plot", click = "click", height = "100%")
)
# Assignment: This app doesn't work! It's supposed to let
# the user click on the plot, and have a data point appear
# where the click occurs. But as written, the data point
# only appears for a moment before disappearing.
#
# This happens because each time the plot is re-rendered,
# the value of input$click is reset to NULL, and thus
# userPoint() becomes NULL as well.
#
# Can you get a single user-added data point to stay?
#
# Bonus points: Can you include not just the single most
# recent click, but ALL clicks made by the user?
#
# Hint: You'll need to replace reactive() with a combo
# of reactiveValues() and observeEvent().
server <- function(input, output, session) {
# Either NULL, or a 1-row data frame that represents
# the point that the user clicked on the plot
userPoint <- reactive({
# input$click will be either NULL or list(x=num, y=num)
click <- input$click
if (is.null(click)) {
# The user didn't click on the plot (or the previous
# click was cleared by the plot being re-rendered)
return(NULL)
}
data.frame(speed = click$x, dist = click$y)
})
output$plot <- renderPlot({
# Before plotting, combine the original dataset with
# the user data. (rbind ignores NULL args.)
df <- rbind(cars, userPoint())
plot(df, pch = 19)
model <- lm(dist ~ speed, df)
abline(model)
})
}
shinyApp(ui, server)
library(shiny)
ui <- fillPage(
plotOutput("plot", click = "click", height = "100%")
)
server <- function(input, output, session) {
# Instead of a reactive expression for userPoint, we
# use a reactive value. This gives us more control
# over when userPoint gets updated.
rv <- reactiveValues(userPoint = NULL)
observeEvent(input$click, {
# Replace rv$userPoint, but only if input$click isn't NULL
if (!is.null(input$click)) {
rv$userPoint <- data.frame(
speed = input$click$x,
dist = input$click$y
)
}
})
output$plot <- renderPlot({
# Now refers to rv$userPoint instead of userPoint().
df <- rbind(cars, rv$userPoint)
plot(df, pch = 19)
model <- lm(dist ~ speed, df)
abline(model)
})
}
shinyApp(ui, server)
library(shiny)
ui <- fillPage(
plotOutput("plot", click = "click", height = "100%")
)
server <- function(input, output, session) {
rv <- reactiveValues(userPoints = NULL)
# Same as Solution_05a.R, but instead of keeping
# track of the single most recent point, we accumulate
# all previous points using rbind().
observeEvent(input$click, {
if (!is.null(input$click)) {
thisPoint <- data.frame(
speed = input$click$x,
dist = input$click$y
)
rv$userPoints <- rbind(rv$userPoints, thisPoint)
}
})
output$plot <- renderPlot({
df <- rbind(cars, rv$userPoints)
plot(df, pch = 19)
model <- lm(dist ~ speed, df)
abline(model)
})
}
shinyApp(ui, server)
those are examples of a “reactive conductor” as they exist in between sources (e.g. an input) and endpoints (e.g. an output).
As such a reactive()
depends on various upstream inputs and can be used to generate output.
Their primary use is similar to a function in an R script, they help to
avoid repeating yourself
decompose complex computations into smaller / more modular steps
improve computational efficiency by breaking up / simplifying reactive dependencies
library(shiny)
ui <- fluidPage(
numericInput(
"nsamples",
"# of samples",
value = 10
),
verbatimTextOutput("s1"),
plotOutput("p1")
)
server <- function(input, output, session) {
output$s1 <- renderPrint({
summary(rnorm(input$nsamples))
})
output$p1 <- renderPlot({
hist(rnorm(input$nsamples))
})
}
shinyApp(ui, server)
What is the problem if we don’t use the reactive()
here?
library(shiny)
ui <- fluidPage(
numericInput(
"nsamples",
"# of samples",
value = 10
),
verbatimTextOutput("s1"),
plotOutput("p1")
)
server <- function(input, output, session) {
samples <- reactive(rnorm(input$nsamples))
output$s1 <- renderPrint({
summary(samples())
})
output$p1 <- renderPlot({
hist(samples())
})
}
shinyApp(ui, server)
A reactive has two important properties:
This means that it only does work when it’s actually needed, and if called twice in a row, it returns the previous value.
Observers and outputs differ from reactive expressions
in two important ways:
Observers and outputs are powered by the same underlying tool: observe()
. This sets up a block of code that is run every time one of the reactive values or expressions it uses is updated.
observe() also powers reactive outputs. Reactive outputs are a special type of observers that have two important properties:
output
, e.g. output$p1
At the simplest level, a module is a pair of UI and server functions.
A piece of a shiny app, that runs inside a shiny application and can represent input, output or both
Shiny modules have two big advantages:
library(shiny)
ui <- fluidPage(
selectInput("var", "Variable", names(mtcars)),
numericInput("bins", "bins", 10, min = 1),
plotOutput("hist")
)
server <- function(input, output, session) {
data <- reactive(mtcars[[input$var]])
output$hist <- renderPlot(
{
hist(data(), breaks = input$bins, main = input$var)
},
res = 96
)
}
shinyApp(ui, server)
library(shiny)
histUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("var"), "Variable", choices = names(mtcars)),
numericInput(ns("bins"), "bins", value = 10, min = 1),
plotOutput(ns("hist"))
)
}
histServer <- function(id) {
moduleServer(id, function(input, output, session) {
data <- reactive(mtcars[[input$var]])
output$hist <- renderPlot(
{
hist(data(), breaks = input$bins, main = input$var)
},
res = 96
)
})
}
ui <- fluidPage(
histUI("hist1")
)
server <- function(input, output, session) {
histServer("hist1")
}
shinyApp(ui, server)
The key idea that makes modules work is that the name of each control is now determined by two pieces:
histUI()
histUI()
library(shiny)
counterButton <- function(id, label = "Counter") {
# complete the module UI part
actionButton("button"), label = label),
verbatimTextOutput("out")
}
counterServer <- function(id) {
# complete the module server part
moduleServer(
id,
function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
})
}
)
}
ui <- fluidPage(
counterButton("counter1", "Counter #1"),
)
server <- function(input, output, session) {
counterServer("counter1")
}
shinyApp(ui, server)
library(shiny)
counterButton <- function(id, label = "Counter") {
ns <- NS(id)
tagList(
actionButton(ns("button"), label = label),
verbatimTextOutput(ns("out"))
)
}
counterServer <- function(id) {
moduleServer(
id,
function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$button, {
count(count() + 1)
})
output$out <- renderText({
count()
})
}
)
}
ui <- fluidPage(
counterButton("counter1", "Counter #1"),
hr(),
counterButton("counter2", "Counter #2"),
hr(),
counterButton("counter3", "Counter #3"),
)
server <- function(input, output, session) {
counterServer("counter1")
counterServer("counter2")
counterServer("counter3")
}
shinyApp(ui, server)
library(shiny)
library(dplyr)
countButtonUI <- function(id) {
ns <- NS(id)
tagList(
fluidRow(
column(
width = 12,
actionButton(ns("like"), label = "Like: ", icon = icon("thumbs-up"), class = "primary"),
actionButton(ns("dislike"), label = "Dislike: ", icon = icon("thumbs-down"), class = "primary")
)
),
fluidRow(
column(
width = 12,
imageOutput(ns("myImage"))
),
)
)
}
countButtonServer <- function(id, image) {
moduleServer(
id,
function(input, output, session) {
like_count <- reactiveVal(0)
observe({
like_count(like_count() + 1)
}) %>%
bindEvent(input$like)
dislike_count <- reactiveVal(0)
observe({
dislike_count(dislike_count() + 1)
}) %>%
bindEvent(input$dislike)
output$myImage <- renderImage(
{
list(
src = image,
height = "60%",
alt = "A pic"
)
},
deleteFile = FALSE
)
observe({
updateActionButton(
inputId = "like", label = paste0("Like: ", like_count()),
)
updateActionButton(
inputId = "dislike", label = paste0("Dislike: ", dislike_count()),
)
})
}
)
}
ui <- fluidPage(
countButtonUI("count1"),
# add some more gifs
)
server <- function(input, output, session) {
countButtonServer("count1", image = "hadley.gif")
# add some more gifs
}
shinyApp(ui, server)
library(shiny)
library(dplyr)
countButtonUI <- function(id) {
ns <- NS(id)
tagList(
fluidRow(
column(
width = 12,
actionButton(ns("like"), label = "Like: ", icon = icon("thumbs-up"), class = "primary"),
actionButton(ns("dislike"), label = "Dislike: ", icon = icon("thumbs-down"), class = "primary")
)
),
fluidRow(
column(
width = 12,
imageOutput(ns("myImage"))
),
)
)
}
countButtonServer <- function(id, image) {
moduleServer(
id,
function(input, output, session) {
like_count <- reactiveVal(0)
observe({
like_count(like_count() + 1)
}) %>%
bindEvent(input$like)
dislike_count <- reactiveVal(0)
observe({
dislike_count(dislike_count() + 1)
}) %>%
bindEvent(input$dislike)
output$myImage <- renderImage(
{
list(
src = image,
height = "60%",
alt = "A pic"
)
},
deleteFile = FALSE
)
observe({
updateActionButton(
inputId = "like", label = paste0("Like: ", like_count()),
)
updateActionButton(
inputId = "dislike", label = paste0("Dislike: ", dislike_count()),
)
})
}
)
}
ui <- fluidPage(
countButtonUI("count1"),
hr(),
countButtonUI("count2"),
hr(),
countButtonUI("count3"),
hr(),
countButtonUI("count4")
)
server <- function(input, output, session) {
countButtonServer("count1", image = "hadley.gif")
countButtonServer("count2", image = "css.gif")
countButtonServer("count3", image = "programming.gif")
countButtonServer("count4", image = "refactoring-cat.gif")
}
shinyApp(ui, server)
Mastering-shiny Chapter 19
Section 19.3.6:
https://ifendo.shinyapps.io/histogramApp/
Section 19.4.3:
If you are creating a large or long-term Shiny app, I highly recommend that you organise your app in the same way as an R package. This means:
R/
directoryDESCRIPTION
file in the root directory of your appHadley, Mastering-Shiny Chapter 20
R package: https://github.com/ThinkR-open/golem
Engineering Production-Grade Shiny Apps: https://engineering-shiny.org/
https://github.com/Biogen-Inc/tidyCDISC
3rd Annual Shiny Contest Grand prizes
https://github.com/ThinkR-open/wedding
https://www.rstudio.com/blog/winners-of-the-3rd-annual-shiny-contest/
tidyCDISCmini
https://www.rstudio.com/resources/shiny-dev-con/reactivity-pt-1-joe-cheng/
https://www.rstudio.com/resources/shiny-dev-con/reactivity-pt-2/
https://mastering-shiny.org/index.html