R Shiny - Part II

Lokesh Mano

NBIS, SciLifeLab

14-Apr-2025

Contents

Reactivity

  • Code doesn’t always run line-by-line (Non-linear execution)
  • Code executes when dependencies change


reactlog

Reactives

Functions with reactive context

  • reactive(): Defines an expression
  • reactiveVal(): Defines single value
  • reactiveValues(): Defines a list of values

Regular function fn <- function(): Runs wherever it is is used
Reactive function fn <- reactive(): Runs only when input changes

Reactive context

Reactive values cannot be accessed outside a reactive context

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui = fluidPage(
    numericInput("num", "Enter number",
                 value = 1),
    textOutput("result")
  ),
  server = function(input, output) {
    val = input$num^2
    output$result <- renderText({
      paste("Squared number is:", val)
    })
  }
)

Reactive dynamics

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui=fluidPage(
    numericInput("num_input",label="Observations",value=50),
    textOutput("text_output")),
  server=function(input,output) {
    output$text_output <- renderText({
        mean(rnorm(input$num_input))
    })
  })

Reactive dynamics

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui=fluidPage(
    numericInput("num_input",label="Observations",value=50),
    textOutput("text_output1"), textOutput("text_output2")),
  server=function(input,output) {
    output$text_output1 <- renderText({
      mean(rnorm(input$num_input))
    })
    output$text_output2 <- renderText({
      mean(rnorm(input$num_input))
    })
  })

Reactive dynamics

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui=fluidPage(
    numericInput("num_input",label="Observations",value=50),
    textOutput("text_output1"), textOutput("text_output2")),
  server=function(input,output) {
    rand <- function(x) rnorm(x)
    output$text_output1 <- renderText({
      mean(rand(input$num_input))
    })
    output$text_output2 <- renderText({
      mean(rand(input$num_input))
    })
  })

Reactive dynamics

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui=fluidPage(
    numericInput("num_input",label="Observations",value=50),
    textOutput("text_output1"), textOutput("text_output2")
    ),
  server=function(input,output) {
    rand <- reactive({ rnorm(input$num_input) })
    output$text_output1 <- renderText({
      mean(rand())
    })
    output$text_output2 <- renderText({
      mean(rand())
    })
  })

Reactive values

  • reactVal() reactValues()
  • Store reactive values that can be accessed from any reactive context
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui = fluidPage(
    actionButton("add","Add"),
    actionButton("subtract","Subtract"),
    textOutput("counter")
  ),
  server = function(input, output) {
    reactive_values <- reactiveValues(counter = 0) 
    
    observeEvent(input$add, {
      reactive_values$counter <- reactive_values$counter + 1
    }) 
    observeEvent(input$subtract, {
      reactive_values$counter <- reactive_values$counter - 1
    }) 
  
    output$counter <- renderText({
      reactive_values$counter
    })
  }
)

Controlling reactivity using action buttons

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
ui = fluidPage(
  h3("Temperature Converter"),
  numericInput("celsius", "Degrees Celsius:", value = 0),
  actionButton("btn_go", "Go!"),
  textOutput("fahrenheit")
),

server = function(input, output) {
  evr <- eventReactive(input$btn_go, {
    paste(input$celsius, "°C is ", (input$celsius * 9/5) + 32, " °F")
  })
  
  output$fahrenheit <- renderText({
    evr()
  })
  
  #output$fahrenheit <- renderText({
  #    paste(input$celsius, "°C is ", (input$celsius * 9/5) + 32, " °F")
  #  }) |>
  #  bindEvent(input$btn_go)
})

Observers

Reactive functions that automatically re-executes whenever its dependencies change. Usually used for side-effects rather than returning a value.

observe()

Doesn’t return a value. Constantly runs as long as the app is alive.

observeEvent()

Similar to observe(), but only re-executes on specific event (like a button click). Doesn’t return a value.

eventReactive()

Similar to observeEvent(), but returns a value.

bindEvent()

Bind an action to a specific event. Similar to observeEvent(), but allows for the event to be determined dynamically. Links an input (like a button) to an output and ensures that reactive expressions are only updated when bound event is triggered.

Updating widgets

  • Widgets can be updated once initialised.
  • Example of a typical UI
ui=fluidPage(
  selectInput("select-input",label="selectInput",choices=c("A","B","C")),
  numericInput("numeric-input",label="numericInput",value=5,min=1,max=10),
  sliderInput("slider-input",label="sliderInput",value=5,min=1,max=10),
)
  • Add third argument session to server function
  • Update functions can be used to update input widgets
  • Reactive observer observe({}) monitors for a conditional change
server=function(input,output,session) {
  observe({
    if(something) {
      updateSelectInput(session,"select-input",label="selectInput",choices=c("D","E","F"))
      updateNumericInput(session,"numeric-input",label="numericInput",value=10,min=1,max=10)
      updateSliderInput(session,"slider-input",label="sliderInput",value=8,min=1,max=10)
    }
  })
}

Updating widgets

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
shinyApp(
  ui = fluidPage(
    selectInput("data", "Choose a dataset:", choices = c("mtcars", "iris", "faithful")),
    selectInput("variable", "Choose a variable:", choices = names(mtcars))
  ),
  
  server = function(input, output, session) {
    
    dataset <- reactive({
      switch(input$data,
             "mtcars" = mtcars,
             "iris" = iris,
             "faithful" = faithful)
    })
    
    observe({
      updateSelectInput(session,"variable",choices=names(dataset()))
    })
  }
)

Updating widgets

Input function Update function
checkboxInput() updateCheckboxInput()
checkboxGroupInput() updateCheckboxGroupInput()
radioButtons() updateRadioButtons()
dateInput() updateDateInput()
dateRangeInput() updateDateRangeInput()
fileInput()
numericInput() updateNumericInput()
sliderInput() updateSliderInput()
textInput() updateTextInput()
textAreaInput() updateTextAreaInput()
passwordInput()
selectInput() updateSelectInput()
actionButton()
submitButton()
tabsetPanel() updateTabsetPanel()
navbarPage() updateNavbarPage()

Error validation

  • Shiny returns an error with missing or incorrect values
shinyApp(
ui=fluidPage(
  selectInput("data_input",label="Select data",
              choices=c("","mtcars","faithful","iris")),
  tableOutput("table_output")
),
server=function(input, output) {
  getdata <- reactive({ get(input$data_input,'package:datasets') })
  output$table_output <- renderTable({head(getdata())})
})

  • Errors can be handled in a controlled manner

Error validation

  • validate() can be used to check input
  • validate() using need()
shinyApp(
ui=fluidPage(
  selectInput("data_input",label="Select data",
              choices=c("","mtcars","faithful","iris")),
  tableOutput("table_output")
),
server=function(input, output) {
  getdata <- reactive({ 
  validate(need(try(input$data_input),"Please select a data set"))
    get(input$data_input,'package:datasets') 
    })
  output$table_output <- renderTable({head(getdata())})
})

Error validation

  • validate() using custom function
valfn <- function(x) if(is.null(x) | is.na(x) | x=="") return("Input data is incorrect.")
shinyApp(
ui=fluidPage(
  selectInput("data_input",label="Select data",
              choices=c("","mtcars","faithful","iris")),
  tableOutput("table_output")
),
server=function(input,output) {
  getdata <- reactive({ 
  validate(valfn(try(input$data_input)))
    get(input$data_input,'package:datasets')
    })
  output$table_output <- renderTable({head(getdata())})
})

  • shiny::req() checks input variable and silently stops execution

Download • Data

  • Add button and downloadHandler() function
shinyApp(
ui=fluidPage(
  selectInput("data_input",label="Select data",
              choices=c("mtcars","faithful","iris")),
  textOutput("text_output"),
  downloadButton("button_download","Download")
),
server=function(input, output) {
  getdata <- reactive({ get(input$data_input, 'package:datasets') })
  output$text_output <- renderText(paste0("Selected dataset: ",input$data_input))
  
  output$button_download <- downloadHandler(
  filename = function() {
     paste0(input$data_input,".csv")
   },
  content = function(file) {
     write.csv(getdata(),file,row.names=FALSE,quote=F)
   })
})
  • Run in system browser if Rstudio browser doesn’t work

Download • Plots

shinyApp(
ui=fluidPage(
  selectInput("data_input",label="Select data",
              choices=c("mtcars","faithful","iris")),
  textOutput("text_output"),
  plotOutput("plot_output",width="400px"),
  downloadButton("button_download", "Download")
),
server=function(input, output) {
  getdata <- reactive({ get(input$data_input, 'package:datasets') })
  output$text_output <- renderText(paste0("Selected dataset: ",input$data_input))
  output$plot_output <- renderPlot({hist(getdata()[, 1])})
  
output$button_download <- downloadHandler(
  filename = function() {
    paste0(input$data_input,".png")
  },
  content = function(file) {
    png(file)
    hist(getdata()[, 1])
    dev.off()
  })
})
  • Run in system browser if Rstudio browser doesn’t work
  • See usage of download buttons

Thank you. Questions?

Slide inspirations: Roy Francis (NBIS, RaukR2024)