Search code examples
rshinyshinydashboard

How to force evaluation in shiny render when generating dynamic number of elements?


I generate a dynamic number of valueBox in my shiny, and this number can change depending of the user input. I managed to handle this with a renderUI where I put the wanted number of valueBoxOutput, and I have an observe that will feed them with the content using renderValueBox.

My problem is: the code in the renderValueBox, for some reason, is actually executed after the observe is finished, so because the renderValueBox is in a loop (to have a dynamic number of them) but the code is executed for all the output after the loop, all my output will get the last value of the loop.

Here is a min reprex:

library(shiny)
library(shinydashboard)
library(shinyWidgets)

# Function

compute <- function(id)
{
  print(paste("Compute ", id))
  return(id)
}

# UI

ui = shinyUI(fluidPage(
  
  titlePanel("Compare"),
  useShinydashboard(),
  
  sidebarLayout(
    sidebarPanel(
      numericInput("numitems", label = "Number of items", min = 1, max = 10, value = 2)
    ),
    mainPanel(
      uiOutput("boxes")
    )
  )
))

# Server

server = shinyServer(function(input, output, session) {
  
  data <- reactiveValues(
    ids = list()
  )
  
  output$boxes <- renderUI({
    print("boxes")
    box_list <- list()
    id_list <- list()
    for(id in 1:(input$numitems)) {
      id_box  <- paste0("box_", id)
      print(paste("boxes - ", id_box))
      id_list <- append(id_list, id_box)
      box_list <- append(
        box_list,
        tagList(
          shinydashboard::valueBoxOutput(id_box)
        )
      )
      data$ids <- id_list
    }
    print("boxes end")
    fluidRow(box_list)
  })
  
  observe({
    print("observe")
    for(id_box in data$ids) {
      print(paste("observe - ", id_box))
      output[[id_box]] <- shinydashboard::renderValueBox(valueBox(id_box, compute(id_box), icon = icon("circle-info"), color = "teal"))
    }
    print("end observe")
  })
  
  
})

# Run

shinyApp(ui = ui , server = server)

Here is the result:

Output of the reprex

And the console output:

Output of the console

As you can see the compute (and the render in general) is done after the end of the observe function, and both output will use the last id_box that were set (so the last loop, box_2), instead of correctly using box_1 and box_2.

I tried using force, computing valueBox outside the render, using reactive lists, nothing worked, because whatever I do the render is evaluated after the observe so only the last loop values will be used no matter what.

Do anyone know a way to force execution during the loop ? Or see another way of achieving the same result ?


Solution

  • Why it's always after spending hald a day on a problem, looking for dozens of posts and forum, don't find anything, finally decide to ask a question... that a few minutes later I finally find an answer.

    Anyway, one way to correct this (found here) is to encapsulate the render inside the local function, like this:

    observe({
        print("observe")
        for(id_box in data$ids) {
          print(paste("observe - ", id_box))
          local({
            tmp <- id_box
            output[[tmp]] <- shinydashboard::renderValueBox(valueBox(tmp, compute(tmp), icon = icon("circle-info"), color = "teal"))
          })
        }
        print("end observe")
      })
    

    Now the compute is still called after the end of the observe, but the tmp variable has the correct value:

    Log console

    The result is what I wanted:

    Shiny result

    For the record, I had already tried to use the local function, but if you don't copy the id_box inside another variable just for the local bloc, it won't work.