Modularizing Shiny app code
By: Joe Cheng
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.
A module’s UI function should be given a name that is suffixed with
UI; for example,
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:
The body of this function looks quite similar to a ui.R file. The main differences are:
- The function body starts with the statement
ns <- NS(id). All UI function bodies should start with this line. It takes the string
idand creates a namespace function.
- 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
inputIdarguments being wrapped in
ns(); you also want to use
ns()when declaring a
plotOutputbrush ID, for example.
- The results are wrapped in
tagList, instead of
pageWithSidebar, etc. You only need to use
tagListif you want to return a UI fragment that consists of multiple UI objects; if you were just returning a
divor some specific input, you could skip
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
"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
UI suffix. Since our UI function was called
csvFileInput, we’ll call our server function
You may notice a lot of similarities to a regular Shiny server function. The first three parameters–
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
output$plot, for example. The
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
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.
callModule(myModule, "myModule1", reactive(input$checkbox1))
Assuming the above
csvFile functions are loaded (more on that in a moment), this is how you’d use them in a Shiny app:
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
callModule function is responsible for creating the namespaced
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
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).
First we’ll make the module UI function. We want two plots,
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.)
The module server function comes next. Besides the mandatory
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 (
To allow the data frame and columns to change in response to user actions, the
right must all be reactive expressions.
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.
To see this module in action, click here.
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
innerUI, notice that the
id argument is
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
Using renderUI within modules
Inside of a module, you may want to use
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?
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.
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.
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.
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.
For more on this topic, see the following resources:
We love it when R users help each other, but RStudio does not monitor or answer the comments in this thread. If you'd like to get specific help, we recommend the RStudio Community as well as the Shiny Discussion Forum for in depth discussion of Shiny related questions and How to get help article for a list of the best ways to get help with R code.comments powered by Disqus