renderUI uiOutput acts differently based on how list of outputs is defined


#1

Hi, I want to use renderUI to make a dynamic number of shinyBS::bsCollapsePanels, and am getting different results based on how I construct the list of bsCollapsePanel results. I am not sure if this behavior is due to the renderUI, renderDataTable, or bsCollapsePanel function.

If I construct a list manually (see output$Works), it works properly, but this does not allow me to specify the number of bsCollapsePanels dynamically.

If I construct a list with a for loop (see output$does_not_work), the last table in the list gets printed in every panel. This is very surprising. Any idea why this is occuring? Even though the code seems like it should generate the exact same list of bsCollapsePanels outputs?

Here is the reprex (R Shiny app):

library(shiny)
library(shinyBS)
library(tidyverse)

shinyApp(
  ui =
    fluidPage(
      fluidRow(uiOutput("works")),
      fluidRow(uiOutput("does_not_work"))
      
    ),
  server =
    function(input, output, session) {
      output$works <- renderUI({
        list_of_tables <- list(tibble(V1 = "a"), tibble(V1 = "b"), tibble(V2 = "c"))
        
        # works, but # of panels is not dynamic
        myCollapse <- list(bsCollapsePanel(1,renderDataTable(list_of_tables[[1]])),
                          bsCollapsePanel(2,renderDataTable(list_of_tables[[2]])),
                          bsCollapsePanel(3,renderDataTable(list_of_tables[[3]])))
        
        do.call(bsCollapse,myCollapse) %>% return()
      })
      output$does_not_work <- renderUI({
        list_of_tables <- list(tibble(V1 = "a"), tibble(V1 = "b"), tibble(V2 = "c"))
        
        # does not work (prints the same table in all panels), and is dynamic
        myCollapse <- vector("list",length = length(list_of_tables))
        for (i in 1:length(list_of_tables)) {
          myCollapse[[i]] <- bsCollapsePanel(i,renderDataTable(list_of_tables[[i]]))
        }
        
        do.call(bsCollapse,myCollapse) %>% return()
      })
    }
)

#2

I don't think I'm able to reproduce your problem. This is what it looks like for me:

Here's my session info. One small change I made to your code before getting the session info: I replaced library(tidyverse) with library(tibble) and library(magrittr), to reduce the number extraneous loaded packages.

> sessioninfo::session_info()
─ Session info ────────────────────────────────────────────────────────────────────────────
 setting  value                       
 version  R version 3.5.0 (2018-04-23)
 os       macOS High Sierra 10.13.4   
 system   x86_64, darwin15.6.0        
 ui       RStudio                     
 language (EN)                        
 collate  en_US.UTF-8                 
 tz       America/Chicago             
 date     2018-06-28                  

─ Packages ────────────────────────────────────────────────────────────────────────────────
 package     * version    date       source                      
 clisymbols    1.2.0      2017-05-21 CRAN (R 3.5.0)              
 crayon        1.3.4      2017-09-16 CRAN (R 3.5.0)              
 digest        0.6.15     2018-01-28 CRAN (R 3.5.0)              
 htmltools     0.3.6      2017-04-28 CRAN (R 3.5.0)              
 httpuv        1.4.4.1    2018-06-18 CRAN (R 3.5.0)              
 jsonlite      1.5        2017-06-01 CRAN (R 3.5.0)              
 later         0.7.3      2018-06-08 CRAN (R 3.5.0)              
 magrittr    * 1.5        2014-11-22 CRAN (R 3.5.0)              
 mime          0.5        2016-07-07 CRAN (R 3.5.0)              
 pillar        1.2.3      2018-05-25 CRAN (R 3.5.0)              
 promises      1.0.1      2018-04-13 CRAN (R 3.5.0)              
 R6            2.2.2      2017-06-17 CRAN (R 3.5.0)              
 Rcpp          0.12.17    2018-05-18 cran (@0.12.17)             
 rlang         0.2.0.9001 2018-06-23 Github (r-lib/rlang@ba4fb06)
 rsconnect     0.8.8      2018-03-09 CRAN (R 3.5.0)              
 rstudioapi    0.7        2017-09-07 CRAN (R 3.5.0)              
 sessioninfo   1.0.0      2017-06-21 CRAN (R 3.5.0)              
 shiny       * 1.1.0      2018-05-17 CRAN (R 3.5.0)              
 shinyBS     * 0.61       2015-03-31 CRAN (R 3.5.0)              
 tibble      * 1.4.2      2018-01-22 CRAN (R 3.5.0)              
 withr         2.1.2      2018-03-15 CRAN (R 3.5.0)              
 xtable        1.8-2      2016-02-05 CRAN (R 3.5.0)         

#3

I can reproduce the problem. I made a few adjustments to the reprex:

  • Followed @winston's lead and cut down the package dependencies
  • Added headers for display clarity
  • Added arguments to bsCollapse() so all the panels are open at once (for ease of screenshotting)
  • Out of curiosity, swapped in both renderTable() and renderPrint() for renderDataTable() — the results are the same for all three. Used renderPrint() for the screenshot below.
library(shiny)
library(shinyBS)
library(tibble)
library(magrittr)

shinyApp(
  ui =
    fluidPage(
      fluidRow(h3("Works"), uiOutput("works")),
      fluidRow(h3("Does not work"), uiOutput("does_not_work"))
    ),
  
  server =
    function(input, output, session) {
      
      output$works <- renderUI({
        list_of_tables <- list(tibble(V1 = "a"), tibble(V1 = "b"), tibble(V2 = "c"))
        
        # works, but # of panels is not dynamic
        myCollapse <- list(bsCollapsePanel(1,renderPrint(list_of_tables[[1]])),
                           bsCollapsePanel(2,renderPrint(list_of_tables[[2]])),
                           bsCollapsePanel(3,renderPrint(list_of_tables[[3]])))

        myCollapse[["multiple"]] <- TRUE
        myCollapse[["open"]] <- c(1, 2, 3)
        
        do.call(bsCollapse, myCollapse) %>% return()
      })
      
      output$does_not_work <- renderUI({
        list_of_tables <- list(tibble(V1 = "a"), tibble(V1 = "b"), tibble(V2 = "c"))
        
        # does not work (prints the same table in all panels), and is dynamic
        myCollapse <- vector("list",length = length(list_of_tables))
        for (i in 1:length(list_of_tables)) {
          myCollapse[[i]] <- bsCollapsePanel(i, renderPrint(list_of_tables[[i]]))
        }
        
        myCollapse[["multiple"]] <- TRUE
        myCollapse[["open"]] <- c(1, 2, 3)
        
        do.call(bsCollapse, myCollapse) %>% return()
      })
    }
)
> sessionInfo()
R version 3.4.4 (2018-03-15)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 16.04.4 LTS

Matrix products: default
BLAS: /usr/lib/atlas-base/atlas/libblas.so.3.0
LAPACK: /usr/lib/atlas-base/atlas/liblapack.so.3.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8        LC_COLLATE=C.UTF-8    
 [5] LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8    LC_PAPER=C.UTF-8       LC_NAME=C             
 [9] LC_ADDRESS=C           LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] magrittr_1.5 tibble_1.4.2 shinyBS_0.61 shiny_1.1.0 

loaded via a namespace (and not attached):
 [1] Rcpp_0.12.17     assertthat_0.2.0 utf8_1.1.4       crayon_1.3.4     digest_0.6.15   
 [6] later_0.7.3      mime_0.5         R6_2.2.2         jsonlite_1.5     xtable_1.8-2    
[11] pillar_1.2.3     cli_1.0.0        rlang_0.2.1      promises_1.0.1   tools_3.4.4     
[16] httpuv_1.4.3     rsconnect_0.8.8  compiler_3.4.4   htmltools_0.3.6 

Just to eliminate the obvious, cat(file = stderr(), unlist(list_of_tables[[i]]), "\n") inside the loop outputs exactly as expected:

a
b
c

#4

I've had some issues with for loops not working as expected in the past. I have no idea why, but if you need a dynamic solution you can use lapply (or purrr::map if you prefer):

library(shiny)
library(shinyBS)
library(tibble)
library(magrittr)

shinyApp(
  ui =
    fluidPage(
      fluidRow(uiOutput("works")),
      fluidRow(uiOutput("also_works"))
      
    ),
  server =
    function(input, output, session) {
      output$works <- renderUI({
        list_of_tables <- list(tibble(V1 = "a"), tibble(V1 = "b"), tibble(V2 = "c"))
        
        # works, but # of panels is not dynamic
        myCollapse <- list(bsCollapsePanel(1,renderDataTable(list_of_tables[[1]])),
                           bsCollapsePanel(2,renderDataTable(list_of_tables[[2]])),
                           bsCollapsePanel(3,renderDataTable(list_of_tables[[3]])))
        
        do.call(bsCollapse,myCollapse) %>% return()
      })
      output$also_works <- renderUI({
        list_of_tables <- list(tibble(V1 = "a"), tibble(V1 = "b"), tibble(V2 = "c"))
        
        myCollapse <- lapply(1:length(list_of_tables), function(x) {
          bsCollapsePanel(x, renderDataTable(list_of_tables[[x]]))
        })
          
        do.call(bsCollapse,myCollapse) %>% return()
      })
    }
)

#5

In R, the for loop iterator is a single variable shared by each loop iteration. When a loop ends, the iterator variable sticks around and contains the last element in the sequence. This often leads to unexpected behavior when creating functions (or closures) in the loop that access the iterator. If these functions are called after the loop ends, they'll always use the final value of the iterator, not the value at the time they were created.

An example -

funcs <- list()

for (i in 1:3)
  funcs[[i]] <- function() print(i)

print(i)
## [1] 3

for (f in funcs)
  f()
## [1] 3
## [1] 3
## [1] 3

That's essentially what's happening here since the Shiny renderXX functions create render functions that aren't called until their associated outputs are ready to show. By the time any of these render functions get called, the loop has already ended and i will be at length(list_of_tables).

        for (i in 1:length(list_of_tables)) {
          myCollapse[[i]] <- bsCollapsePanel(i, renderPrint(list_of_tables[[i]]))
        }

You can get around this by creating a new scope for each loop iteration using local() or a function. I made some examples of this a while ago: Shiny app with dynamic number of datatables

But I recommend using the apply functions (like @paul showed) over for-loops whenever possible. I think it's the easiest and most predictable.