0

When working on a larger app that has Shiny modules that include modules themselves, I've noticed that I can't put a downloadbutton in a renderUI call without it being disabled. Here's an example:

library(shiny)




# Module - top level
topLevel_UI <- function(id) {
  ns <- NS(id)
  tagList(
    dl_UI(ns("my_module"))
  )
}

topLevel_Server <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      dl_Server("my_module")
    }
  )
}

# Module - lower level

dl_UI <- function(id) {
  ns <- NS(id)
  tagList(
    uiOutput(ns("download_button"))
  )
}

dl_Server <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      ns <- NS(id)
      
      output$download_button <- renderUI({
        downloadButton(ns("download"), label = "Download file")
      })
      
      output$download <- downloadHandler(
        filename = function() {"test.csv"},
        content = function(file) {
          write.csv(iris, file)
        }
      )
    }
  )
}


# Main app

ui <- fluidPage(
  h1("Application"),
  topLevel_UI("app")
  # Including the lower level directly works
  # , dl_UI("low")
)

server <- function(input, output, session) {
  topLevel_Server("app")
  # Including the lower level directly works
  # , dl_Server("low")
}

shinyApp(ui, server)

When running the app, the button is disabled and cannot be clicked. If the module wasn't nested in topLevel module but called directly in main shiny app it works as intended (the comments in the code show this type of calling the module).

How can I fix it? It's probably due to namespacing issues.
I need the downloadButton to be in a renderUI call so I can have more logic before it.

1 Answer 1

4

You are correct. It's namespacing issue. In your dl_Server function, the local definition of ns should be

      ns <- session$ns

not

      ns <- NS(id)

With that change, your app works as expected. For further details see this page, specifically, the section Using renderUI within modules, on the Posit website.

Edit in response to OP's question in comment

You can't use NS and session$ns interchangably. They do different things - at least to an extent. NS("id") returns a function. session$ns("id") returns a string. [But, within a top level module with an id of "mod", NS("mod")("id"), session$ns("id") and NS("mod", "id") all return the same value: "mod-id".]

I think the problem has a number of causes. First, as I've demonstrated above, NS called with a single argument returns a function. When called with two arguments, it returns a string.

Further, Shiny documentation, examples and source code write code such as

modUI <- function(id) {
  ns <- NS(id)
  ...
}

But the first parameter to NS is named not id but namespace:

> shiny::NS
function (namespace, id = NULL) 
{
    if (length(namespace) == 0) 
        ns_prefix <- character(0)
    else ns_prefix <- paste(namespace, collapse = ns.sep)
    f <- function(id) {
        if (length(id) == 0) 
            return(ns_prefix)
        if (length(ns_prefix) == 0) 
            return(id)
        paste(ns_prefix, id, sep = ns.sep)
    }
    if (missing(id)) {
        f
    }
    else {
        f(id)
    }
}
<bytecode: 0x130c46b00>
<environment: namespace:shiny>

Of course, a namespace is also an id, so the call is not incorrect, but it is misleading unless you have good understanding of what's going on under the hood.

So, in a top level module you could ignore either sessions$ns or NS, but not both. session$ns is a convenience that means you don't need to worry about the namespace within which the call is made. You don't have that luxury when using NS.

The subtlety comes when modules are nested. In a nested module NS(id) ignores the module hierarchy. session$ns does not. That explains why, in your example, you need session$ns.

For an extra level of detail, you can see how session$ns gets set by looking at the source of ShinySession$makeScope, which is called by shiny::callModule within shiny::moduleServer.

Sign up to request clarification or add additional context in comments.

1 Comment

Amazing, it works! Thank you very much! On the page, it's not really explained WHY this is done. Can I just use session$ns always and forget about NS()?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.