Modularizing Shiny app code
As Shiny applications grow larger and more complicated, we use modules to manage the growing complexity of Shiny application code.
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, and you can write functions to be used in the server function that define outputs and create reactive expressions.
In practice, though, functions alone don’t address the problem of organising and managing large and complex app code completely. 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.
Shiny modules address the namespacing problem in Shiny UI and server logic, adding a level of abstraction beyond functions.
Note: Prior to Shiny 1.5.0, we recommended using callModule()
to invoke modules. From 1.5.0 onward, we recommend using moduleServer()
instead, because it has simpler syntax for using the module. Additionally, new-style modules can be tested with the testServer()
function, also introduced in Shiny 1.5.0. If you would like to see how to migrate from old to new-style modules and see how they compare, see the Migrating from callModule
to moduleServer
section, below.
A Simple Module
Here’s a small application demonstrating a simple “counter” module:
library(shiny)
<- function(id, label = "Counter") {
counterButton <- NS(id)
ns tagList(
actionButton(ns("button"), label = label),
verbatimTextOutput(ns("out"))
)
}
<- function(id) {
counterServer moduleServer(
id,function(input, output, session) {
<- reactiveVal(0)
count observeEvent(input$button, {
count(count() + 1)
})$out <- renderText({
outputcount()
})
count
}
)
}
<- fluidPage(
ui counterButton("counter1", "Counter #1")
)
<- function(input, output, session) {
server counterServer("counter1")
}
shinyApp(ui, server)
NS()
is used withincounterButton()
to encapsulate the module’s UI.counterServer()
is a function which callsmoduleServer()
.- The call to
moduleServer()
is passed theid
, as well as a module function. - In this particular example, the module function returns a reactive value, but it could return any value.
- Inside of the applications
server
function,counterServer()
is called to initialize the module.
If that all makes sense to you, great! Otherwise, read on for more details.
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). One possible motivation for building modules is to bundle them into R packages to be used by other Shiny authors. Another possible motivation is to break up a complicated Shiny app into separate modules that can each be reasoned about independently; such modules likely have no potential for reuse but they serve an important purpose within an app.
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, csvFileUI
, 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. 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
<- function(id, label = "CSV file") {
csvFileUI # `NS(id)` returns a namespace function, which was save as `ns` and will
# invoke later.
<- NS(id)
ns
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 the UI code for a Shiny app. 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 stringid
and creates a namespace function. - All input and output IDs that appear in the function body needs to be wrapped in a call to
ns()
. This example showsinputId
arguments being wrapped inns()
, e.g.ns("file")
). If we happened to have aplotOutput
in our UI, we would also want to usens()
when declaring itsoutputId
orbrush
ID, for example. - The results are wrapped in
tagList
, instead offluidPage
,pageWithSidebar
, etc. You only need to usetagList
if you want to return a UI fragment that consists of multiple UI objects; if you were just returning adiv
or a single input, you could skiptagList
.
Admittedly, the ns()
mechanism isn’t very elegant, but what it buys us makes it worth it. 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 that we’ll call the module server function.
Module server functions should be named like their corresponding module UI functions, but with a server
suffix instead of a Input
/Output
/UI
suffix. Since our UI function was called csvFileUI
, we’ll call our server function csvFileServer
.
Inside of csvFileServer
, there is a call to moduleServer()
, to which two things are passed. One is the id
, and the second is the module function:
# Module server function
<- function(id, stringsAsFactors) {
csvFileServer moduleServer(
id,## Below is the module function
function(input, output, session) {
# The selected file, if any
<- reactive({
userFile # If no file is selected, don't do anything
validate(need(input$file, message = FALSE))
$file
input
})
# The user's data, parsed into a data frame
<- reactive({
dataframe read.csv(userFile()$datapath,
header = input$heading,
quote = input$quote,
stringsAsFactors = stringsAsFactors)
})
# We can run observers in here if we want to
observe({
<- sprintf("File %s was uploaded", userFile()$name)
msg cat(msg, "\n")
})
# Return the reactive that yields the data frame
return(dataframe)
}
) }
The outer function, csvFileServer()
, takes id
as its first parameter. You can define the function so that it takes any number of additional parameters, including ...
, so that whoever uses the module can customize what the module does. In this case, there’s one extra parameter, stringsAsFactors
, so the application that uses this module can decide whether or not to convert strings to factors when reading in the data.
Inside of csvFileServer()
is a call to the moduleServer()
function. This function is passed the id
variable, as well as the module function. You may notice a lot of similarities between the module function and a regular Shiny server function. Its three parameters – input
, output
, and session
– should be familiar: every module function must take those three parameters. The moduleServer()
function invokes the module function in a special way that creates special input
, output
, and session
objects that are aware of the id
.
Inside of the module function, it can use parameters from the outside function. In this example, this outside function is csvFileServer()
, and its stringsAsFactors
parameter is used inside the module function. 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 module function, 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 use the id
to scope them 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.
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, the outer function should take the reactive expression as a 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(...)
):
myModule("myModule1", reactive(input$checkbox1))
Using modules
Assuming the above csvFileUI
and csvFileServer
functions are loaded (more on that in a moment), this is how you’d use them in a Shiny app:
library(shiny)
<- fluidPage(
ui sidebarLayout(
sidebarPanel(
csvFileUI("datafile", "User data (.csv format)")
),mainPanel(
dataTableOutput("table")
)
)
)
<- function(input, output, session) {
server <- csvFileServer("datafile", stringsAsFactors = FALSE)
datafile
$table <- renderDataTable({
outputdatafile()
})
}
shinyApp(ui, server)
The UI function csvFileUI
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 called with csvFileServer()
, with the id
that we will use as the namespace; this must be exactly the same as the id
argument we passed to csvFileUI
. The call to the module server function also is passed the parameter stringsAsFactors = FALSE
.
Like all Shiny modules, csvFileUI
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 call to csvFileServer()
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.)
<- function(id) {
linkedScatterUI <- NS(id)
ns
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. These parameters are passed to linkedScatterServer
, and they can be used in the module function defined inside.
<- function(id, data, left, right) {
linkedScatterServer moduleServer(
id,function(input, output, session) {
# Yields the data frame with an additional column "selected_"
# that indicates whether that observation is brushed
<- reactive({
dataWithSelection brushedPoints(data(), input$brush, allRows = TRUE)
})
$plot1 <- renderPlot({
outputscatterPlot(dataWithSelection(), left())
})
$plot2 <- renderPlot({
outputscatterPlot(dataWithSelection(), right())
})
return(dataWithSelection)
}
) }
Notice that the module function inside of linkedScatterServer()
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.
<- function(data, cols) {
scatterPlot ggplot(data, aes_string(x = cols[1], y = cols[2])) +
geom_point(aes(color = selected_)) +
scale_color_manual(values = c("black", "#66D65C"), guide = FALSE)
}
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")
:
<- function(id) {
innerUI <- NS(id)
ns "This is the inner UI"
}
<- function(id) {
outerUI <- NS(id)
ns 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()
.
<- function(id) {
innerServer moduleServer(
id,function(input, output, session) {
# inner logic here
}
)
}
<- function(id) {
outerServer moduleServer(
id,function(input, output, session) {
<- innerServer("inner1")
innerResult # outer logic here
}
) }
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.
<- function(id) {
columnChooserUI <- NS(id)
ns uiOutput(ns("controls"))
}
<- function(id, data) {
columnChooserServer moduleServer(
id,function(input, output, session) {
$controls <- renderUI({
output<- session$ns
ns selectInput(ns("col"), "Columns", names(data), multiple = TRUE)
})
return(reactive({
validate(need(input$col, FALSE))
$col]
data[,input
}))
}
) }
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.
In an R script in the R/ subdirectory
You can create a separate R script (.R file) for the module in the R/ subdirectory of your application. It will automatically be sourced (as of Shiny 1.5.0) when the application is loaded.
This is the recommended method for modules that won’t be reused across applications.
In an R script elsewhere in the app directory
You can create a separate R script (.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 environment.
In versions of Shiny prior to 1.5.0, this was the recommended method, but with 1.5.0 and later, we recommend the previous method, where the standalone R script is in the R/ subdirectory.
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 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.
Migrating from callModule
to moduleServer
Prior to Shiny 1.5.0, the only way to use the module function was with callModule
; as of Shiny 1.5.0, we added the moduleServer
function and recommend using it instead of callModule
, because the syntax for the user of the module is more consistent with the UI portion, and somewhat easier to understand. (Note that the UI portion of modules was unchanged.) New-style modules also can be tested with the testServer
function, also introduced in Shiny 1.5.0.
Here is an example of the older-style module. The part to pay attention to is the definition of myModule
, and its corresponding use in the server function, with callModule
:
# Module definition, old method
<- function(id, label = "Input text: ") {
myModuleUI <- NS(id)
ns tagList(
textInput(ns("txt"), label),
textOutput(ns("result"))
)
}
<- function(input, output, session, prefix = "") {
myModule $result <- renderText({
outputpaste0(prefix, toupper(input$txt))
})
}
# Use the module in an application
<- fluidPage(
ui myModuleUI("myModule1")
)<- function(input, output, session) {
server callModule(myModule, "myModule1", prefix = "Converted to uppercase: ")
}shinyApp(ui, server)
Here is the same application, with the new method using moduleServer()
. This time, we are creating a function called myModuleServer
, and in the application’s server function, we call that function directly (instead of using callModule()
.
# Module definition, new method
<- function(id, label = "Input text: ") {
myModuleUI <- NS(id)
ns tagList(
textInput(ns("txt"), label),
textOutput(ns("result"))
)
}
<- function(id, prefix = "") {
myModuleServer moduleServer(
id,function(input, output, session) {
$result <- renderText({
outputpaste0(prefix, toupper(input$txt))
})
}
)
}
# Use the module in an application
<- fluidPage(
ui myModuleUI("myModule1")
)<- function(input, output, session) {
server myModuleServer("myModule1", prefix = "Converted to uppercase: ")
}shinyApp(ui, server)
The old and new versions of the application have the exact same behavior. Notice, however, that with the new version, the usage of the module in the application is more consistent, with myModuleUI("myModule1")
and myModuleServer("myModule1", prefix = "Converted to uppercase: ")
.
When creating the module server function, here is the old method:
# Old-style modules
<- function(input, output, session, prefix = "") {
myModule $result <- renderText({
outputpaste0(prefix, toupper(input$txt))
}) }
And here is the new method:
# New-style modules
<- function(id, prefix = "") {
myModuleServer moduleServer(
id,function(input, output, session) {
$result <- renderText({
outputpaste0(prefix, toupper(input$txt))
})
}
) }
In the old version, the myModule
function takes input
, output
, and session
, as parameters, along with any additional user parameters – in this case, prefix
. In the new version, the function just takes id
and additional user parameters (prefix
).
In the new version, there is an inner module function that takes input
, output
, and session
, and no other parameters. The code from the old-style module is simply moved into this inner module function. Any extra user parameters, like prefix
(or even ...
) can be accessed inside that function because they are available in the parent environment.
When it comes to using the module in the application’s server function, here is the old method, which uses Shiny’s callModule
function:
# Old-style modules
<- function(input, output, session) {
server callModule(myModule, "myModule1", prefix = "Converted to uppercase: ")
}
The new method is more straightforward: it simply calls the myModuleServer
function.
# New-style modules
<- function(input, output, session) {
server myModuleServer("myModule1", prefix = "Converted to uppercase: ")
}
Learn more
For more on this topic, see the following resources: