Modularizing Shiny app code

As Shiny applications grow larger and more complicated, app authors frequently ask us for techniques, patterns, and recommendations for managing the growing complexity of Shiny application code.

In the past, we’ve responded rather glibly to these requests: “Just use functions!” Functions are the fundamental unit of abstraction in R, and we designed Shiny to work with them. You can write UI-generating functions and call them from your app’s ui.R, and you can write functions for server.R that define outputs and create reactive expressions.

In practice, though, functions alone don’t solve enough of the problem. Input and output IDs in Shiny apps share a global namespace, meaning, each ID must be unique across the entire app. If you’re using functions to generate UI, and those functions generate inputs and outputs, then you need to ensure that none of the IDs collide.

In computer science, the traditional solution to the problem of name collisions is namespaces. As long as names are unique within a namespace, and no two namespaces have the same name, then each namespace/name combination is guaranteed to be unique. Many systems will let you nest namespaces, so a namespace doesn’t need a name that’s globally unique, just unique within its parent namespace.

This proposal adds namespacing to Shiny UI and server logic via a new feature: Shiny modules.

Introducing Shiny Modules

A Shiny module is a piece of a Shiny app. It can’t be directly run, as a Shiny app can. Instead, it is included as part of a larger app (or as part of a larger Shiny module–they are composable).

Modules can represent input, output, or both. They can be as simple as a single output, or as complicated as a multi-tabbed interface festooned with controls/outputs driven by multiple reactive expressions and observers.

Once created, a Shiny module can be easily reused–whether across different apps, or multiple times in a single app (like a set of controls that needs to appear on multiple tabs of a complex app). Modules can even be bundled into R packages and used by other Shiny authors. Other Shiny modules will be created that have no potential for reuse, by simply breaking up a complicated Shiny app into separate modules that can each be reasoned about independently.

Creating Shiny Modules

A module is composed of two functions that represent 1) a piece of UI, and 2) a fragment of server logic that uses that UI–similar to the way that Shiny apps are split into UI and server logic.

Indeed, the contents of your UI and server functions will look a lot like normal Shiny UI/server logic. But the packaging needs to differ in a few important ways.

Creating UI

A module’s UI function should be given a name that is suffixed with Input, Output, or UI; for example, csvFileInput, zoomableChoroplethOutput, or tabOneUI.

The first argument to a UI function should always be id. This is the namespace for the module. (Note that the namespace for the module is decided by the caller at the time the module is used, not decided by the author at the time the module is written. This will make more sense later, when we talk about how modules are invoked.)

Here’s an example for a CSV file input module:

# Module UI function
csvFileInput <- function(id, label = "CSV file") {
  # Create a namespace function using the provided id
  ns <- NS(id)

  tagList(
    fileInput(ns("file"), label),
    checkboxInput(ns("heading"), "Has heading"),
    selectInput(ns("quote"), "Quote", c(
      "None" = "",
      "Double quote" = "\"",
      "Single quote" = "'"
    ))
  )
}

The body of this function looks quite similar to a ui.R file. The main differences are:

  1. The function body starts with the statement ns <- NS(id). All UI function bodies should start with this line. It takes the string id and creates a namespace function.
  2. Anything input or output ID of any kind that appears in the function body needs to be wrapped in a call to ns(). This example shows inputId arguments being wrapped in ns(); you also want to use ns() when declaring a plotOutput brush ID, for example.
  3. The results are wrapped in tagList, instead of fluidPage, pageWithSidebar, etc. You only need to use tagList if you want to return a UI fragment that consists of multiple UI objects; if you were just returning a div or some specific input, you could skip tagList.

Admittedly, the ns() mechanism isn’t very elegant. What it buys us makes it worth it, though. Thanks to the namespacing, we only need to make sure that the IDs "file", "heading", and "quote" are unique within this function, rather than unique across the entire app.

Writing server functions

Now that we’ve got some UI, we can turn our attention to the server logic. The server logic is encapsulated in a single function we’ll call the module server function.

Module server functions should be named like their corresponding module UI functions, but without the Input/Output/UI suffix. Since our UI function was called csvFileInput, we’ll call our server function csvFile:

# Module server function
csvFile <- function(input, output, session, stringsAsFactors) {
  # The selected file, if any
  userFile <- reactive({
    # If no file is selected, don't do anything
    validate(need(input$file, message = FALSE))
    input$file
  })

  # The user's data, parsed into a data frame
  dataframe <- reactive({
    read.csv(userFile()$datapath,
      header = input$heading,
      quote = input$quote,
      stringsAsFactors = stringsAsFactors)
  })

  # We can run observers in here if we want to
  observe({
    msg <- sprintf("File %s was uploaded", userFile()$name)
    cat(msg, "\n")
  })

  # Return the reactive that yields the data frame
  return(dataframe)
}

You may notice a lot of similarities to a regular Shiny server function. The first three parameters–input, output, and session–should be familiar. They’re required in every module’s server function (session isn’t optional, as it is in a Shiny app server function). The next parameter is specific to this example; you can have as many or as few additional parameters as you want, including ... if it makes sense, and you can use them for whatever you want inside the function body.

Inside the function body, we can use input$file to refer to the ns("file") component in the UI function. If this example had outputs, we could similarly match up ns("plot") with output$plot, for example. The input, output, and session objects we’re provided with are special, in that they are scoped to the specific namespace that matches up with our UI function.

On the flip side, the input, output, and session cannot be used to access inputs/outputs that are outside of the namespace, nor can they directly access reactive expressions and reactive values from elsewhere in the application (OK, technically, lexically scoped reactive expressions/values can be used, but that’s it).

These restrictions are by design, and they are important. The goal is not to prevent modules from interacting with their containing apps, but rather, to make these interactions explicit. If a module needs to use a reactive expression, take the reactive expression as a function parameter. If a module wants to return reactive expressions to the calling app, then return a list of reactive expressions from the function.

If a module needs to access an input that isn’t part of the module, the containing app should pass the input value wrapped in a reactive expression (i.e. reactive(...)):

callModule(myModule, "myModule1", reactive(input$checkbox1))

Using modules

Assuming the above csvFileInput and csvFile functions are loaded (more on that in a moment), this is how you’d use them in a Shiny app:

library(shiny)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      csvFileInput("datafile", "User data (.csv format)")
    ),
    mainPanel(
      dataTableOutput("table")
    )
  )
)

server <- function(input, output, session) {
  datafile <- callModule(csvFile, "datafile",
    stringsAsFactors = FALSE)

  output$table <- renderDataTable({
    datafile()
  })
}

shinyApp(ui, server)

The UI function csvFileInput is called directly, using "datafile" as the id. In this case, we’re inserting the generated UI into the sidebar.

The module server function is not called directly; instead, call the callModule function, and provide the module server function as the first argument. The second argument to callModule is the ID that we will use as the namespace; this must be exactly the same as the id argument we passed to csvFileInput. The callModule function is responsible for creating the namespaced input, output, and session arguments.

Like all Shiny modules, csvFileInput can be embedded in a single app more than once. Each call must be passed a unique id, and each call must have a corresponding callModule on the server side with that same id.

Output example

Here’s an example of a module that consists of two linked scatterplots (selecting an area on one plot will highlight observations on both plots).

library(shiny)
library(ggplot2)

First we’ll make the module UI function. We want two plots, plot1 and plot2, side-by-side with a common brush ID of brush. (Notice that the brush ID needs to be wrapped in ns(), just like the plotOutput IDs.)

linkedScatterUI <- function(id) {
  ns <- NS(id)

  fluidRow(
    column(6, plotOutput(ns("plot1"), brush = ns("brush"))),
    column(6, plotOutput(ns("plot2"), brush = ns("brush")))
  )
}

The module server function comes next. Besides the mandatory input, output, and session parameters, we need to know the data frame to plot (data), and the column names that should be used as x and y for each plot (left and right).

To allow the data frame and columns to change in response to user actions, the data, left, and right must all be reactive expressions.

linkedScatter <- function(input, output, session, data, left, right) {
  # Yields the data frame with an additional column "selected_"
  # that indicates whether that observation is brushed
  dataWithSelection <- reactive({
    brushedPoints(data(), input$brush, allRows = TRUE)
  })

  output$plot1 <- renderPlot({
    scatterPlot(dataWithSelection(), left())
  })

  output$plot2 <- renderPlot({
    scatterPlot(dataWithSelection(), right())
  })

  return(dataWithSelection)
}

Notice that the linkedScatter function returns the dataWithSelection reactive. This means that the caller of this module can make use of the brushed data as well, such as showing it in a table below the plots, for example.

For clarity and ease of testing, let’s put the plotting code in a standalone function. The scale_color_manual call sets the colors of unselected vs. selected points, and guide = FALSE hides the legend.

scatterPlot <- function(data, cols) {
  ggplot(data, aes_string(x = cols[1], y = cols[2])) +
    geom_point(aes(color = selected_)) +
    scale_color_manual(values = c("black", "#66D65C"), guide = FALSE)
}

To see this module in action, click here.

Nesting modules

Modules can use other modules. When doing so, when the outer module’s UI function calls the inner module’s UI function, ensure that the id is wrapped in ns(). In the following example, when outerUI calls innerUI, notice that the id argument is ns("inner1"):

innerUI <- function(id) {
  ns <- NS(id)
  "This is the inner UI"
}

outerUI <- function(id) {
  ns <- NS(id)
  wellPanel(
    innerUI(ns("inner1"))
  )
}

As for the module server functions, just ensure that the call to callModule for the inner module happens inside the outer module’s server function. There’s generally no need to use ns().

inner <- function(input, output, session) {
  # inner logic
}
outer <- function(input, output, session) {
  innerResult <- callModule(inner, "inner1")
  # outer logic
}

Using renderUI within modules

Inside of a module, you may want to use uiOutput/renderUI. If your renderUI block itself contains inputs/outputs, you need to use ns() to wrap your ID arguments, just like in the examples above. But those ns instances were created using NS(id), and in this case, there’s no id parameter to use. What to do?

The session parameter can provide the ns for you; just call ns <- session$ns. This will put the ID in the same namespace as the session.

columnChooserUI <- function(id) {
  ns <- NS(id)
  uiOutput(ns("controls"))
}

columnChooser <- function(input, output, session, data) {
  output$controls <- renderUI({
    ns <- session$ns
    selectInput(ns("col"), "Columns", names(data), multiple = TRUE)
  })

  return(reactive({
    validate(need(input$col, FALSE))
    data[,input$col]
  }))
}

Packaging modules

The previous examples of using a module assume that the module’s UI and server functions are defined and available. But logistically, where should these functions actually be defined, and how should they be loaded into R?

There are several options.

Inline code

Most simply, you can put the UI and server function code directly in your app.

If you’re using an app.R style file layout (both app UI and server logic in the same file), then you can just include the code for your module functions right in that file, before the app’s UI and server logic.

If you’re using a ui.R/server.R style file layout, add a global.R file to your app directory (if you don’t already have one) and put the UI and server functions there. The global.R file will be loaded before either ui.R or server.R.

If you have many modules to define, or modules that contain a lot of code, this may result in a bloated global.R/app.R file.

Standalone R file

You can create a separate .R file for the module, either directly in the app directory or in a subdirectory. Then call source("path-to-module.R") from global.R (if using ui.R/server.R) or app.R. This will add your module functions to the global namespace.

This is probably the best approach for modules that won’t be reused across applications.

R package

For modules that are intended for reuse across applications, consider building an R package. If you’ve never done this before, a good resource is Hadley Wickham’s book R Packages, which is freely available online.

Your R package simply needs to export and document your module’s UI and server functions. You can include more than one module in a package, if you like.

Learn more

For more on this topic, see the following resources:

Understanding Shiny Modules Modularizing Shiny app code



If you have questions about this article or would like to discuss ideas presented here, please post on RStudio Community. Our developers monitor these forums and answer questions periodically. See help for more help with all things Shiny.


Start
Build
Improve