rshinyreactive-programmingrenderui

Understanding renderUI in Shiny


I have a Shiny app that allows the user to load a csv and then plot the data. I then have some custom functions to create different UI elements, that are passed into the server, for example:

   GroupByUI <- function(id,
                      data,
                      label = 'Group By',
                      selected = 'All',
                      None = FALSE,
                      All = TRUE) {
  renderUI({
    xsecs <- data %>%
      select_if(is.character) %>%
      names()

    if (None) {
      xsecs <- c('None', xsecs)
    }

    if (All == FALSE) {
      xsecs <- xsecs[-which(xsecs == 'All')]

      selectInput(inputId = paste0('groupby_', id),
                  label = label,
                  choices = as.list(xsecs),
                  selected = as.list(xsecs)[1])

    } else {

      selectInput(inputId = paste0('groupby_', id),
                  label = label,
                  choices = as.list(xsecs),
                  selected = selected)
    }
  })

} 

and in the server I have:

output$GroupBy_Area <- GroupByUI(
  id = "area",
  data = dataFlt()
)

and this works when I load a first csv (e.g. the Group By selectInput is updated with the correct variable names).

For context, dataFlt() is a reactive and it's set to NULL and updates every time I load a new/different csv.

dataFlt <- reactive({
  req(uploadedData$data)
  datafl <- uploadedData$data
})

The problem that I have is, that if I have loaded a csv, plotted the data and then I change the csv that I load (e.g. I change the data I want to plot, variables' names change), the options in the Group By selectInput do not update accordingly UNLESS I move the renderUI directly to the server:

output$GroupBy_Area <- renderUI({
  req(dataFlt())
  GroupByUI(id = "area", 
            data = dataFlt())
})

and I remove the renderUI from the function:

GroupByUI <- function(id, data, label = 'Group By', selected = 'All', None = FALSE, All = TRUE) {
  xsecs <- data %>%
    select_if(is.character) %>%
    names()

  if (None) {
    xsecs <- c('None', xsecs)
  }

  if (All == FALSE) {
    xsecs <- xsecs[-which(xsecs == 'All')]

    selectInput(inputId = paste0('groupby_', id),
                label = label,
                choices = as.list(xsecs),
                selected = as.list(xsecs)[1])

  } else {
    selectInput(inputId = paste0('groupby_', id),
                label = label,
                choices = as.list(xsecs),
                selected = selected)
  }
}

With this latest code (moving the renderUI directly to the server), it works every time I change the data I load.

What I don't understand is why. What's the difference? And why do the original code works the first time?


Solution

  • A very simple solution is to pass the reactive object itself, and not its value, to the GroubByUI function:

    output$GroupBy_Area <- GroupByUI(
      id = "area",
      data = dataFlt
    )
    

    And modify the function to "call" the reactive and get its value:

       GroupByUI <- function(id,
                          data,
                          label = 'Group By',
                          selected = 'All',
                          None = FALSE,
                          All = TRUE) {
      renderUI({
        xsecs <- data() %>%
          select_if(is.character) %>%
          names()
    
        if (None) {
          xsecs <- c('None', xsecs)
        }
    
        if (All == FALSE) {
          xsecs <- xsecs[-which(xsecs == 'All')]
    
          selectInput(inputId = paste0('groupby_', id),
                      label = label,
                      choices = as.list(xsecs),
                      selected = as.list(xsecs)[1])
    
        } else {
    
          selectInput(inputId = paste0('groupby_', id),
                      label = label,
                      choices = as.list(xsecs),
                      selected = selected)
        }
      })
    
    } 
    

    This way, the renderUI function takes a dependency on the reactive and should update accordingly.

    Explanation

    The not-reactive code in the server function is actually only run once, at the start of the session. In order for objects like renderUI, observers etc to update correctly, they need to have a direct dependency on the reactive themselves. They get this dependency if the reactive object is called directly inside them, but in your code, dataFlt() is called by your function, which does not work in a reactive context, and not by the renderUI function, that then doesn't take a dependency on it and doesn't update when you think it should.

    I hope this helps :)