Search code examples
rshinyshiny-reactivity

Why is observe not being triggered by a reactive value change?


My code is below.

The expected behavior is such:

  1. the action button run_analysis is clicked by the user.
  2. This triggers the observeEvent block for input$run_analysis, which updates the reactive value run_analysis_clicked to TRUE.
  3. The change in this reactive value should then trigger the observe block to be executed, which updates the selected tab in the mainPanelTabs tabset.

But that's not what's happening. For some reason, the observe block is NOT being triggered after the run_analysis_clicked value is changed.

I've tried to pair this down to a minimally reproducible sample.

library(shiny)

#### USER INTERFACE ####
ui <- fluidPage(
    titlePanel("Bootstrap Sample Size Simulator"),
    sidebarLayout(
        sidebarPanel(
            # Define the primary inputs needed to setup the sample size simulation
            fileInput("file_upload", "Upload a CSV file with sample data to use for sample size optimization:", accept = ".csv"),
            uiOutput("slider_output"),
            uiOutput("action_output")
        ),
        mainPanel(
            tabsetPanel(
                id = "mainPanelTabs",  # Add an ID for easier reference
                tabPanel("instructions", "Instructions content goes here."),
                tabPanel("data", "Data content goes here."),
                tabPanel("results", "Results content goes goes here.")
            )
        )
    )
)

#### SERVER ####
server <- function(input, output, session) {
    # Define reactive values that populate when the file is uploaded
    upload <- reactiveValues(
        isUploaded = FALSE, # flag signifying the file has been uploaded
        data = NULL, # empty placeholder for the data
        sample_max = NULL # initialize as empty
    )
    
    # Define reactive value that identifies when the run_analysis button has been clicked
    run_analysis_clicked <- reactive({
        FALSE # Flag is initially set to FALSE
    })
    
    # Define reactive value that updates the selected tab
    selectedTab <- reactiveVal("instructions")
    
    # Define function for creating the slider UI element for sample size limits selection
    slider_ui <- function() {
        if (upload$isUploaded) {
            sliderInput("sample_range",
                        label = "Range of Sample Sizes to simulate from the uploaded data:",
                        min = 2,
                        max = upload$sample_max,
                        step = 1,
                        value = c(2, upload$sample_max)
            )
        } else {
            NULL
        }
    }
    
    # Define function for creating the run analysis button
    action_ui <- function() {
        if (upload$isUploaded) {
            actionButton("run_analysis",
                         label = "Run the analysis"
            )
        } else {
            NULL
        }
    }
    
    # Update reactive values when file is uploaded
    observeEvent(input$file_upload, {
        upload$isUploaded <- TRUE  # update flag after file is uploaded
        upload$data <- read.csv(input$file_upload$datapath) # read the uploaded CSV file
        upload$sample_max <- nrow(upload$data) # update sample_max after file has been 
        print(paste("upload$sample_max:",upload$sample_max))
    })
    
    # Update reactive values when button is clicked
    observeEvent(input$run_analysis, {
                 run_analysis_clicked <- TRUE # update flag after button clicked
                 print(paste("run_analysis_clicked",run_analysis_clicked))
                 })
    
    # Update selected tab based on upload and button click events
    observe({
        print("observe triggered")
        print(paste("upload$isUploaded:",upload$isUploaded))
        print(paste("run_analysis_clicked:",run_analysis_clicked()))
        if (!upload$isUploaded) {
            selectedTab("instructions")
        } else if (run_analysis_clicked() == FALSE) {
            selectedTab("data")
        } else {
            selectedTab("results")
        }
        
        # Update the selected tab directly in the UI
        updateTabsetPanel(session, "mainPanelTabs", selected = selectedTab())
    })
    
    # Render the slider and action buttons conditionally upon file upload
    output$slider_output <- renderUI({
        slider_ui()
    })
    output$action_output <- renderUI({
        action_ui()
    })
}

#### APP ####
shinyApp(ui = ui, server = server)

Here's the console output (with added comments explaining the actions that triggered the outputs):

Listening on http://xxxxxx
# App Initialized: 
[1] "observe triggered"
[1] "upload$isUploaded: FALSE"
[1] "run_analysis_clicked: FALSE"
# File Uploaded:
[1] "upload$sample_max: 15"
[1] "observe triggered"
[1] "upload$isUploaded: TRUE"
[1] "run_analysis_clicked: FALSE"
# Button Clicked:
[1] "run_analysis_clicked TRUE"

For the file upload, I'm just using a CSV file with 15 random numbers I created in Excel. Here's what it looks like:

Sample Data
0.590024857449706
0.0728674995484038
0.481874325987865
0.960001135837481
0.294927454278591
0.25254752567793
0.460322873384411
0.00237222444026342
0.169595016393134
0.444750644528156
0.033684417887163
0.973733565927954
0.917744500279373
0.264506821656346
0.998370147928976

Solution

  • The one main important thing here to understand is that

    run_analysis_clicked <- reactive({
            FALSE # Flag is initially set to FALSE
        })
    

    does not define a reactive value, it defines a reactive expression. If you define a variable as such a reactive, it is not possible to change its value outside of its reactive environment. You can imagine it as that you have only "read" permissions on a reactive, while on a reactiveVal you have "read" and "write" permissions.

    Since you define the reactive as FALSE, nowhere inside your app a change of this variable can be observed. A possibility how this could be achieved would be to define a dependency to a reactiveVal, e.g.

    rv <- reactiveValues(blnRunAnalysisClicked = TRUE)
    run_analysis_clicked <- reactive({rv$blnRunAnalysisClicked})
    

    But in your case we can skip the reactive part and rely directly on a reactiveVal, e.g. as given by the below example (upload$run_analysis_clicked), which will work.

    library(shiny)
    
    #### USER INTERFACE ####
    ui <- fluidPage(
      titlePanel("Bootstrap Sample Size Simulator"),
      sidebarLayout(
        sidebarPanel(
          # Define the primary inputs needed to setup the sample size simulation
          fileInput("file_upload", "Upload a CSV file with sample data to use for sample size optimization:", accept = ".csv"),
          uiOutput("slider_output"),
          uiOutput("action_output")
        ),
        mainPanel(
          tabsetPanel(
            id = "mainPanelTabs",  # Add an ID for easier reference
            tabPanel("instructions", "Instructions content goes here."),
            tabPanel("data", "Data content goes here."),
            tabPanel("results", "Results content goes goes here.")
          )
        )
      )
    )
    
    #### SERVER ####
    server <- function(input, output, session) {
      # Define reactive values that populate when the file is uploaded
      upload <- reactiveValues(
        isUploaded = FALSE, # flag signifying the file has been uploaded
        data = NULL, # empty placeholder for the data
        sample_max = NULL, # initialize as empty
        run_analysis_clicked = FALSE 
      )
      
      # Define reactive value that updates the selected tab
      selectedTab <- reactiveVal("instructions")
      
      # Define function for creating the slider UI element for sample size limits selection
      slider_ui <- function() {
        if (upload$isUploaded) {
          sliderInput("sample_range",
                      label = "Range of Sample Sizes to simulate from the uploaded data:",
                      min = 2,
                      max = upload$sample_max,
                      step = 1,
                      value = c(2, upload$sample_max)
          )
        } else {
          NULL
        }
      }
      
      # Define function for creating the run analysis button
      action_ui <- function() {
        if (upload$isUploaded) {
          actionButton("run_analysis",
                       label = "Run the analysis"
          )
        } else {
          NULL
        }
      }
      
      # Update reactive values when file is uploaded
      observeEvent(input$file_upload, {
        upload$isUploaded <- TRUE  # update flag after file is uploaded
        upload$data <- read.csv(input$file_upload$datapath) # read the uploaded CSV file
        upload$sample_max <- nrow(upload$data) # update sample_max after file has been 
        print(paste("upload$sample_max:",upload$sample_max))
      })
      
      # Update reactive values when button is clicked
      observeEvent(input$run_analysis, {
        upload$run_analysis_clicked <- TRUE # update flag after button clicked
        print(paste("run_analysis_clicked", upload$run_analysis_clicked))
      })
      
      # Update selected tab based on upload and button click events
      observe({
        print("observe triggered")
        print(paste("upload$isUploaded:",upload$isUploaded))
        print(paste("run_analysis_clicked:",upload$run_analysis_clicked))
        if (!upload$isUploaded) {
          selectedTab("instructions")
        } else if (upload$run_analysis_clicked == FALSE) {
          selectedTab("data")
        } else {
          selectedTab("results")
        }
        
        # Update the selected tab directly in the UI
        updateTabsetPanel(session, "mainPanelTabs", selected = selectedTab())
      })
      
      # Render the slider and action buttons conditionally upon file upload
      output$slider_output <- renderUI({
        slider_ui()
      })
      output$action_output <- renderUI({
        action_ui()
      })
    }
    
    #### APP ####
    shinyApp(ui = ui, server = server)