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()
})
}
)
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.
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()
})
}
)
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):
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.