Shiny app with dynamic number of datatables

Beware of using closures in for loops! The expressions passed into render functions are captured in closures and not evaluated immediately (same goes for reactive and observe). Issues arise when these expressions reference loop variables, which are in the same scope as the for loop and not local to each iteration.

So by the time any of the output expressions are evaluated, the loop has already ended with i = 4, and each table[[i]] expression evaluates to table[[4]].


Here are a few ways around this using a simplified version of your example and faked out Shiny output / renderDataTable for demo purposes.

output <- new.env()
renderTable <- function(expr) {
  function() expr
}

table <- list(
  iris[1, 1:2],
  iris[2, 1:2],
  iris[3, 1:2],
  iris[4, 1:2]
)
# The original problem - each output turns out to be table[[4]]

for (i in seq_len(4)) {
  id <- paste0("dt", i)
  output[[id]] <- renderTable(table[[i]])
}

for (func in as.list(output)) print(func())
#   Sepal.Length Sepal.Width
# 4          4.6         3.1
#   Sepal.Length Sepal.Width
# 4          4.6         3.1
#   Sepal.Length Sepal.Width
# 4          4.6         3.1
#   Sepal.Length Sepal.Width
# 4          4.6         3.1

Create a local scope for each loop iteration

Each table[[i]] references an i bound in a new environment instead of the actual loop variable. This is also demonstrated in @makis23's example.

for (i in seq_len(4)) {
  local({
    i <- i
    id <- paste0("dt", i)
    output[[id]] <- renderTable(table[[i]])
  })
}

for (func in as.list(output)) print(func())
#   Sepal.Length Sepal.Width
# 1          5.1         3.5
#   Sepal.Length Sepal.Width
# 2          4.9           3
#   Sepal.Length Sepal.Width
# 3          4.7         3.2
#   Sepal.Length Sepal.Width
# 4          4.6         3.1

Call the render function in a separate function

Each table[[i]] references an i created in a function environment for each iteration instead of the actual loop variable. But now you also have to deal with function arguments being lazy in R. force is used to force evaluation of function args.

renderMyTable <- function(i) {
  force(i)
  renderTable(table[[i]])
}

for (i in seq_len(4)) {
    id <- paste0("dt", i)
    output[[id]] <- renderMyTable(i)
}

for (func in as.list(output)) print(func())
#   Sepal.Length Sepal.Width
# 1          5.1         3.5
#   Sepal.Length Sepal.Width
# 2          4.9           3
#   Sepal.Length Sepal.Width
# 3          4.7         3.2
#   Sepal.Length Sepal.Width
# 4          4.6         3.1

Or maybe more naturally, without having to explicitly force argument evaluation:

setTableOutput <- function(i) {
  id <- paste0("dt", i)
  output[[id]] <- renderTable(table[[i]])
}

for (i in seq_len(4)) {
  setTableOutput(i)
}

for (func in as.list(output)) print(func())
#   Sepal.Length Sepal.Width
# 1          5.1         3.5
#   Sepal.Length Sepal.Width
# 2          4.9           3
#   Sepal.Length Sepal.Width
# 3          4.7         3.2
#   Sepal.Length Sepal.Width
# 4          4.6         3.1

Use apply (or similar) instead

This is the hakuna matata solution where you don't have to stress out about scoping issues or lazy evaluation (as of R 3.2.0).

lapply(seq_len(4), function(i) {
  id <- paste0("dt", i)
  output[[id]] <- renderTable(table[[i]])
})

for (func in as.list(output)) print(func())
#   Sepal.Length Sepal.Width
# 1          5.1         3.5
#   Sepal.Length Sepal.Width
# 2          4.9           3
#   Sepal.Length Sepal.Width
# 3          4.7         3.2
#   Sepal.Length Sepal.Width
# 4          4.6         3.1
3 Likes