5 Modularization

At the simplest level, a module is a pair of UI and server functions. The magic of modules comes because these functions are constructed in a special way that creates a “namespace”. So far, when writing an app, the names (ids) of the controls are global: all parts of your server function can see all parts of your UI. Modules give you the ability to create controls that can only be seen from within the module. This is called a namespace because it creates “spaces” of “names” that are isolated from the rest of the app.

Shiny modules have two big advantages:

  • Namespacing makes it easier to understand how your app works because you can write, analyse, and test individual components in isolation.
  • Modules are functions that help you reuse code; anything you can do with a function, you can do with a module.

5.1 Module Basics

Unlike regular Shiny code, connecting modules together requires you to be explicit about inputs and outputs. Initially, this is going to feel tiresome and it’s certainly more work than Shiny’s usual free-form association. But modules enforce specific lines of communication for a reason: they’re a little more work to create, but much easier to understand, and allow you to build substantially more complex apps.

To showcase modularization let’s look at a very simple app from mastering shiny:

As we begin to modularize this app you’ll notice that a module is very similar to an app, that is, they are composed of two parts:

  • A module UI function
  • A module server function

These functions both require an id argument that will namespace the module when we use it in our app. To begin creating a module you are going to look for UI/server pairs in your app and extract them out into paired UI/server functions! In our example, this will be our inputs and plot.

Read more!

NOTE

You can test your modules by treating them like a simple standalone app!

5.1.1 Module UI

First let’s set up the UI module. There are two steps:

  • Put the UI in a function with an id argument
  • Wrap each existing id in a NS() function so that each previous id turns into NS(id, "previous_id")

This would look like this for our example:

The UI components are wrapped in tagList(), which allows you to bundle components that can then be placed in other functions in the app code such as shiny::fluidRow(), bslib::layout_column_wrap() or bslib::sidebar(). You can also wrap them in shiny::fluidRow() or bslib::layout_column_wrap() in the function itself if you’d prefer, though that can limit the reusability.

Read more!

NOTE

There is another way to namespace within a UI function though this does add extra steps and can make calling these namespaces in the server a little more tricky:

5.1.2 Module server

The second part of the module is the server function, which is also going to have an id argument; we will use the id to link it to our UI component later when building our app. Inside of this function we call moduleServer() with the id arugment, and an additional function that looks like a typical server function:

These two levels help distinguish the arguments between your module and the server function. They may look complex but this is how every module is set up, so it’s really a copy and paste scenario.

Read more!

NOTE

shiny::moduleServer() takes care of the namespacing automatically, that is, we can refer to NS(id, "var") and NS(id, "bins") from the UI section of the module by calling input$var and input$bins. This is because of the shared id argument between the functions, that we will connect when we implement them in our app like usual.

5.1.3 Module testing

It’s good practice to test your module in a function that creates a simple app:

NOTE

Just like a regular app we need to link the UI and server using the same id name. This will be what allows the UI and server parts of the module to talk to eachother and identify the inputs and outputs.

5.1.4 Namespacing

Let’s circle back on namespacing. At this point of the example we have two separate namespacing situations:

  • Inside of our module we have NS(id, "var"), NS(id, "bins"), and NS(id, "hist")
  • Outside of our module in our app function we have histogramUI("hist1") and histogramServer("hist1")

It’s important to realize here that the namespaces that are created inside a module only exist (ie. can be referred to) within the two module functions; in other words, a module is in essence a “black box” that can’t be seen from outside of the module. This means that as an author of a module, you don’t need to worry about conflicts with namespacing with other modules or with namespacing in the app UI or server.

Even though modules are a “black box” you can still use outside inputs by adding additional function arguments or module outputs to drive functionality with other modules or server elements - we’ll talk about this later in section 5.2.

Read more!

NOTE

Note that the module UI and server differ in how the namespacing is expressed:

  • In the module UI, the namespacing is explicit: you have to call NS(id, "name") every time you create an input or output.
  • In the module server, the namespacing is implicit. You only need to use id in the call to moduleServer() and then Shiny automatically namespaces input and output so that in your module code input$name refers to the input with NS(id, "name").

5.1.5 Single object modules

When some people encounter modules for the first time, they may attempt to combine the module server and module UI into a single module object. However, in Shiny, UI and server are inherently disconnected; Shiny doesn’t know which UI invocation belongs to which server session. You can see this pattern throughout Shiny: for example, plotOutput() and renderPlot() are connected only by shared id. Writing modules as separate functions reflects that reality: they’re distinct functions that are not connected other than through a shared id. This also allows you to make them more generalizable and allows for reactivity.

For a full example read more here.

5.1.6 Module naming conventions

A standard convention for naming can be as follows:

  • R/histogram.R holds all the code for the module.
  • histogramUI() is the module UI. If it’s used primarily for input or output it can be called histogramInput() or histogramOuput() instead.
  • histogramServer() is the module server.
  • histogramApp() creates a complete app for interactive experimentation and more formal testing.

Read more!

5.2 Advanced Modules

Adding arguments beyond the id to the module UI and server gives greater control over the module, allowing you to use the same module in more places in your app. Further, you can return one or more reactive values from your server module so that you can use them in your larger app server call.

Read more!

5.2.2 Server outputs

Now let’s look at the server function. When we are planning to use an output from a server module we will need to wrap it in a shiny::reactive() as it’s technically a reactive output given it’s relationship with the UI. The example sets up the server like so:

5.2.3 Operationalizing advanced modules

Here we put the two parts together. The UI portion is fairly simple, with the only difference being the filter argument was included in the larger app call as well. However, the server part is a bit more complicated. When you are returning a value in the server portion of your app from your module you must assign it to a value in the environment, just like you would in typical R code (ie. x <- mymodule_server()). Because we are working with shiny, and the returned values from modules are reactive, the challenge is deciding if you want to refer to the reactive aspect of the value or the value itself.. let me explain:

  • When you want to use that stored value/object (ie. the dataset or value itself) you must refer to it as value() or data() for example.
  • When you want to use the reactive aspect to drive more server logic/behaviour you would just refer to value or data

Let’s see how this looks in the example:

Read more!

NOTE

In the above example it called the module output as data() when rendering it into the table because we wanted to use the dataset to create our table and not the reactive aspect of the data. The reactive forms are often used for other function arguments or in wrappers like shiny::observeEvent().

5.2.4 Server inputs

Sometimes we want to use other module outputs or outputs created in our larger server call in a module. We can do this by adding an argument in our top level server function. We also need to consider what kind of value we want to use for that argument (ie. reactive versus the actual values); for this reason it can be a good idea to set up simple error messages in your server modules to ensure that future users know what type of value is expected in a server argument:

Read more!

NOTE

Another way to think about reactive or non-reactive values is when can the value change: is it fixed and constant over the life-time of the app, or is it reactive, changing as the user interacts with the app.

5.2.5 Multiple server outputs

Sometimes it’s useful for the module’s server to return more than one value. You can easily do this the same way you would for a regular R function - return a list. The example from mastering shiny is:

Read more!

WARNING

The main challenge with this sort of code is remembering when you use the reactive form (e.g. x$value) vs. when you use its stored value (e.g. x$value()). Just remember that if you need to drive behaviour as a result of that value, you will need the reactive form, you can then call the value from that reactive value as needed. However, if the function only needs the value then consider requiring the non-reactive value for the argument!

5.2.6 Modules inside of modules

Modules can also be nested, in that you can call a module within another module. This makes sense if you have two components that are inherently tied together in your app. Here is an example from mastering shiny:

Read more!

TIP

Nesting can be a really good idea because functionally it requires us to break down our code to the smallest possible building blocks. The two main benefits are:

  • We can easily build new larger functions with these smaller components.
  • We can test these smaller components before combining them - troubleshooting code in shiny is a bit more challenging that typical R, so this makes it easier!

5.3 Examples

To ground some of our knowledge let’s look at some examples used in our apps. These aren’t necessarily perfect so try to think of ways you could improve them and test out your ideas!

5.3.3 Domain select module

This next module looks at doing something similar to the dropdown module:

domain_list <- list(
  "Demographics" = c("Population projection", "Housing"),
  "Factors that affect health" = c("Body mass index adult", "Body mass index youth")
)

# Setting up the UI with just an id argument
# hmm.. there 
domain_select_ui <- function(id) {
  # The other way!
  ns <- shiny::NS(id)
  # Setting this up in a container
  # this is fine but also can restrict it's resuability in the future
  shiny::fluidRow(
    # Domain selection dropdown
    shiny::selectizeInput(
      ns("domain"), # for this example with this alternative method the namespace is specified because we have more than one element
      "Select domain", # the title
      choices = names(domain_list), # initial list
      selected = names(domain_list)[1] #the initial selection
    ),
    # Second dropdown for selection
    shiny::selectizeInput(
      ns("sub_domain"),
      "Select subdomain", # the title
      choices = NULL # empty because updating later on
    )
  )
}


## Server setup, we are looking to link our top level selection with the lower level
domain_select_server <- function(id) {
  shiny::moduleServer(
    id,
    function (input, output, session) {
      # Update HSDA selectizeInput list when
      # a new domain is selected
      shiny::observeEvent(input$domain, {
        # Look at the list and extract the sub contents
        # based on the domain
        choices <- domain_list[[input$domain]]
        # Update the sub domain list based on this selection
        shiny::updateSelectizeInput(
          session,
          "sub_domain",
          choices = shiny::isolate(choices), # I'm not sure we actually need this isolate() call.. try it without!
          server = FALSE,
          selected = choices[1]
        )
      })
      # return a list of values
      return <- shiny::reactiveValues()
      shiny::observe({
        return$domain <- shiny::req(input$domain) # No need for the weird code in the first example to refer to the namespace
        return$sub_domain <- shiny::req(input$sub_domain)
      })
      return(return)
    })
}


## Test demo --------------------------------------------------
domain_select_demo <- function() {
  ui <- shiny::fluidPage(
    domain_select_ui(
      id = "domain_select"
    ),
    shiny::htmlOutput("domain"),
    shiny::htmlOutput("sub_domain")
  )
  # server
  server <- function(input, output, session) {
    domain <- domain_select_server(
      id  = "domain_select"
    )
    shiny::observeEvent(domain, {
      output$domain <- shiny::renderText({domain$domain})
      output$sub_domain <- shiny::renderText({domain$sub_domain})
    })
  }
  shiny::shinyApp(ui, server)
}

domain_select_demo()

5.4 Additional Resources