Search code examples
rshinymodule

What is current best practice for R Shiny namespace modules?


I'm wrestling with the best template to use going forward for my namespace modules in R Shiny. I show two sets of code below that do the exact same thing using namespace modules, Example 1 and Example 2. They use different structures. They both simply add a value of 10 to the slider input values for the sake of easy example. Example 1 is one alternative that someone pointed me to. Example 2 is the method that I've been using, and I find it easier to visualize than Example 1, but I was told it is an outdated form of module.

Is Example 2 an outdated method? If true, why would Example 1 be a better method? Is there an optimal method these days?

Example 1:

library(shiny)

mod1_ui <- function(id) {
  ns <- NS(id)
  tagList(textOutput(ns("result_x")))
}

mod1_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    observe({
      session$userData$result_x(session$userData$x_value() + 10)
    })
    output$result_x <- renderText({
      paste("Mod 1 Result for X = X + 10:", session$userData$result_x())
    })
  })
}

# Main App:
ui <- fluidPage(
  mainPanel(
    sliderInput("x_value", "X Value:", min = 0, max = 10, value = 0),
    mod1_ui("mod1")
  )
)

server <- function(input, output, session) {
  session$userData$x_value <- reactiveVal(0)
  session$userData$result_x <- reactiveVal(0)
  
  observeEvent(input$x_value, {session$userData$x_value(input$x_value)})
  
  mod1_server("mod1")
}

shinyApp(ui, server)

Example 2:

library(shiny)

mod1_ui <- function(id) {
  ns <- NS(id)
  tagList(textOutput(ns("result_x")))
}

mod1_server <- function(id, common, input) {
  moduleServer(id, function(input, output, session) {
    output$result_x <- renderText({
      paste("Mod 1 Result for X = X + 10:", common$x_value() + 10)
    })
  })
}

# Main App:
ui <- fluidPage(
  mainPanel(
    sliderInput("x_value", "X Value:", min = 0, max = 10, value = 0),
    mod1_ui("mod1")
  )
)

server <- function(input, output, session) {
  common <- reactiveValues(
    x_value = reactive(input$x_value)
  )
  
  mod1_data <- mod1_server("mod1", common, input)
}

shinyApp(ui, server)

Solution

  • I wouldn't say one of the methods is outdated / better than the other.

    Method 1 makes the data in session$userData available to all active modules of a given session. If your app has a lot of nested modules that do something with the same set of data, then using this method will make it easier to define all of the nested modules and such without having to pass the same argument to their server functions over and over again.

    Method 2, as you said, makes it easier to visualize what's happening, where a certain object is coming from, what it does etc. It is better suited to pass on arguments that are used by one / a couple of modules.

    A couple of examples:

    Nested modules that use the same data

    Let's say you have an app with double or triple nested modules.

    - app
    -- mod_1
    --- mod_2
    ---- mod_3
    

    You compute in your main server function some data that is only used by mod_3. If you were to pass this data to the module via method 2, you would have to pass it as an argument to the three server functions, and it would only ever be used by the server function of the third module. This increases the verbosity of your code and is not actually useful.

    This would be a nice use case for method 1: you store the data in session$userData and you don't have to pass it as an argument to all these functions that don't really need it.

    Sub modules

    Now let's say you have the same app structure as before, but what you need to do is to pass some filtering options (options) defined in mod_2 to mod_3. If these options are only ever going to be used by mod_3, and are of no interest to the other modules, it doesn't really make sense to use method 1. You can simply use method 2 and pass the options as an argument to mod_3, which also makes it way easier to understand where these options were defined.

    Other thoughts

    While method 1 is certainly useful in some situations, it does have a big drawback: it breaks the modularity of the module. What I mean is that when using this method, the modules are not self-sufficient or independent anymore, as the module server is now using some data that is not clearly defined in the module context. This makes it harder to reuse modules for different applications.

    If this is not a problem for you, then feel free to use it: it is very powerful and can definitely simplify the whole data-transferring process between modules of a complex app.

    Personally, I would advise you to use method 2 for simpler apps, and a combination of both methods for more complex apps with multiple levels of nested modules. This is what usually works best for me :)