rserverdownload

Shiny downloadHandler does not download updated table when called from moduleServer in a separate script


In short: If downloadHandler is used directly inside the server function, then the download works as expected; but if I use downloadHandler in a separate script in a modular way (inside a moduleServer), then when I download a file it works the 1st time but then if I update the table and try to download the new table, the new downloaded file still contains the info of the 1st table. So as soon as I download the data for the 1st time, regardless of which filtering or grouping I do in the app afterwards, when I try to download the "new" table it will always download the same original downloaded table.

PD: This is not just true for .xlsx files but also for .csv files using write.csv()

Detailed: I always go for the module approach when building shiny apps but I have encounter a problem for some time now when building the downloadHandler module. For this module, I have an independent script with the following functions:

download_button_UI <- function(id, label = "", class = "btn-primary btn-xs", ...) {
  ns <- NS(id)
  downloadButton(ns("down_button"),
                 label = label,
                 icon = icon("download"),
                 class = class,
                 title = "Download",
                 ...)
}


download_button_Server <- function(id, df, name, ...) {

  require(openxlsx)
  
  moduleServer(id,
               
    function(input, output, session) {
      output$down_button <- downloadHandler(
       filename = function() name,
       # content = function(file) fwrite(df, file)
       content = function(file) openxlsx::write.xlsx(df, file)
      )
    }
  )
}

Then I use it in the UI and Server respectively:

# its a dashboardPage so the UI is in the "body" part of the page
body <- dashboardBody(

  # some body parts

  # Analysis table output
  
  download_button_UI("down_analysis", "")

  # some more body parts

)

server <- function(input, output, session) {

  # some code

  download_button_Server(id = "down_analysis", df = analysis_tbl()$df, name = "analysis.xlsx")

  # some more code

}

This works as expected for the 1st download of the data; but then If I change the filters and grouping/aggregation of my data and try to download the "new" updated table, the resulting .xlsx file still contains the info of the 1st downloaded table.

If I use the downloadHandler directly inside the server function, then there is no issue and the last updated table is downloaded:

# its a dashboardPage so the UI is in the "body" part of the page
body <- dashboardBody(

  # some body parts

  # Analysis table output
  
  downloadButton("down_analysis", "")

  # some more body parts

)

server <- function(input, output, session) {

  # some code

  output$down_analysis <- downloadHandler(
      filename = "analysis.xlsx",
      content = function(file) write.xlsx(summ_tbl()$df, file)
  )

  # some more code

}

Then the code works as expected.

Of course you might say that then I should just use the downloadHandler inside the server but I have several tables and download buttons in the app so I don't want to be repeating the same code over and over, and the module solution is simply always better and cleaner.

I don't know if this is a shiny bug and if somebody else has encounter a similar problem; I have not found this issue mentioned in any other site and as I said, I encountered it a couple of years ago and it is still persisting so I would like to solve it once and for all.


Solution

  • You've passed the value via analysis_tbl()$df to the server module, which means that no update will be registered. You may want to pass in the entire reactive value analysis_tbl. Calling analysis_tbl() then should always give the most up-to-date values (see, i.e., this answer).

    Your module server then could look like this:

    # passing analysis_tbl reactive instead of df itself
    download_button_Server <- function(id, analysis_tbl, name, ...) {
    
      require(openxlsx)
      
      moduleServer(id,
                   
        function(input, output, session) {
          df <- analysis_tbl()$df
          output$down_button <- downloadHandler(
           filename = function() name,
           content = function(file) openxlsx::write.xlsx(df, file)
          )
        }
      )
    }
    

    And for your app:

    server <- function(input, output, session) {
    
      # some code
    
      output$down_analysis <- downloadHandler(
          filename = "analysis.xlsx",
          content = function(file) write.xlsx(summ_tbl, file)
      )
    
      # some more code
    
    }