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:
ui <- shiny::fluidPage(
shiny::selectInput("var", "Variable", names(datasets::mtcars)),
shiny::numericInput("bins", "bins", 10, min = 1),
shiny::plotOutput("hist")
)
server <- function(input, output, session) {
data <- shiny::reactive(datasets::mtcars[[input$var]])
output$hist <- shiny::renderPlot({
hist(
data(),
breaks = input$bins,
main = input$var
)
},
res = 96
)
}
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.
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 aNS()
function so that each previousid
turns intoNS(id, "previous_id")
This would look like this for our example:
histogramUI <- function(id) {
htmltools::tagList(
shiny::selectInput(shiny::NS(id, "var"), "Variable", choices = names(mtcars)),
shiny::numericInput(shiny::NS(id, "bins"), "bins", value = 10, min = 1),
shiny::plotOutput(shiny::NS(id, "hist"))
)
}
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.
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:
# your first line will look like this, you may have more
# arguments, we'll look at that later
histogramServer <- function(id) {
# your second line will always look like this
shiny::moduleServer(id, function(input, output, session) {
# you put your server code inside of this call
data <- shiny::reactive(datasets::mtcars[[input$var]])
output$hist <- shiny::renderPlot({
hist(data(), breaks = input$bins, main = input$var)
}, res = 96)
})
}
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.
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:
histogramApp <- function() {
ui <- shiny::fluidPage(
histogramUI("hist1")
)
server <- function(input, output, session) {
histogramServer("hist1")
}
shiny::shinyApp(ui, server)
}
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")
, andNS(id, "hist")
- Outside of our module in our app function we have
histogramUI("hist1")
andhistogramServer("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.
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 tomoduleServer()
and then Shiny automatically namespacesinput
andoutput
so that in your module codeinput$name
refers to the input withNS(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 forinput
oroutput
it can be calledhistogramInput()
orhistogramOuput()
instead.
histogramServer()
is the module server.
histogramApp()
creates a complete app for interactive experimentation and more formal testing.
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.
5.2.1 Additional UI arguments
Let’s look at an example from the book where we will have an additional argument in our UI function. We do it the same way as any other function, we add it as an argument in our function call:
# Two arguments, id as usual and a filter
datasetInput <- function(id, filter = NULL) {
# Get list of df names for user selection
names <- ls("package:datasets")
# If a filter is present then update the names in the list
# accordingly
if (!is.null(filter)) {
# get the data to identify datasets vs matrices
data <- lapply(names, get, "package:datasets")
# filter for selection, "dataset" or "matrix"
names <- names[vapply(data, filter, logical(1))]
}
# Set up the UI selections for the list of names
shiny::selectInput(shiny::NS(id, "dataset"), "Pick a dataset", choices = names)
}
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:
datasetServer <- function(id) {
# set up the usual
shiny::moduleServer(id, function(input, output, session) {
# The last expression in the function
# will be the return value and return values
# should always be inside a reactive() wrapper
shiny::reactive(get(input$dataset, "package:datasets"))
})
}
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()
ordata()
for example. - When you want to use the reactive aspect to drive more server logic/behaviour you would just refer to
value
ordata
Let’s see how this looks in the example:
# They included an argument in the larger app call
# it makes sense for this small example but you would probably
# call it in the UI itself.. at least I would
datasetApp <- function(filter = NULL) {
ui <- shiny::fluidPage(
# Set up the input portion of the module
datasetInput("dataset", filter = filter),
# also include a placeholder for a table to show
# the selected data from our module
shiny::tableOutput("data")
)
server <- function(input, output, session) {
# Set up the server, because it's giving us a reactive value
# that we want to use in the table we need to do `<-` assignment
# just like we would normally in R
data <- datasetServer("dataset")
# Now call this data in our table render
output$data <- renderTable(head(data()))
}
shinyApp(ui, server)
}
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:
selectVarServer <- function(id, data, filter = is.numeric) {
# We need data to be reactive to drive functionality!
stopifnot(shiny::is.reactive(data))
# We need the filter to be a non-reactive value!
stopifnot(!shiny::is.reactive(filter))
shiny::moduleServer(id, function(input, output, session) {
shiny::observeEvent(data(), {
# Now we call the actual data using data() and filter as normal as it was never
# reactive
shiny::updateSelectInput(session, "var", choices = find_vars(data(), filter))
})
shiny::reactive(data()[[input$var]])
})
}
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:
selectVarServer <- function(id, data, filter = is.numeric) {
stopifnot(shiny::is.reactive(data))
stopifnot(!shiny::is.reactive(filter))
moduleServer(id, function(input, output, session) {
shiny::observeEvent(data(), {
shiny::updateSelectInput(session, "var", choices = find_vars(data(), filter))
})
list(
# Return the name and the values of the variable
name = shiny::reactive(input$var),
value = shiny::reactive(data()[[input$var]])
)
})
}
histogramApp <- function() {
ui <- shiny::fluidPage(...)
server <- function(input, output, session) {
data <- datasetServer("data")
# assign the list to X
x <- selectVarServer("var", data)
# create a histogram using the values and title it with the
# variable name. We refer to the reactive forms of the values
# because within this server module the values are called from the reactive
histogramServer("hist", x$value, x$name)
}
shinyApp(ui, server)
}
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:
# Because the example's goal is to allow users to select
# a dataset and then select a variable from that dataset
# it made sense to just combine the individual modules into a larger
# module
selectDataVarUI <- function(id) {
# Tag list for combining multiple components
htmltools::tagList(
# This is a dataset input selection
datasetInput(shiny::NS(id, "data"), filter = is.data.frame),
# This is to select the variable from the selected dataset
selectVarInput(shiny::NS(id, "var"))
)
}
selectDataVarServer <- function(id, filter = is.numeric) {
shiny::moduleServer(id, function(input, output, session) {
# Here we pull the data based on the selection
data <- datasetServer("data")
# and here we filter the data for the variable
var <- selectVarServer("var", data, filter = filter)
var #return value
})
}
selectDataVarApp <- function(filter = is.numeric) {
ui <- shiny::fluidPage(
shiny::sidebarLayout(
# Now there only needs to be a single call in the UI
shiny::sidebarPanel(selectDataVarUI("var")),
shiny::mainPanel(shiny::verbatimTextOutput("out"))
)
)
server <- function(input, output, session) {
# and a single call in the server
var <- selectDataVarServer("var", filter)
output$out <- shiny::renderPrint(var(), width = 40)
}
shinyApp(ui, server)
}
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.1 Dropdown module
First let’s break down a simple module that helps create a dropdown menu. We will go over the module and then consider ways that we could improve it according to the mastering shiny documentation.
Here is the original module:
# For this module we first set up the UI
# As always we include that id argument, but
# we also have a label argument so we can customize
# our menu title depending on what we're using it for
dropdown_ui <- function(id, label) {
# Here's the alternative method, the idea of this is that
# it will use the `id` from the function argument to create the
# internal namespace instead of just declaring it right here. This is only possible
# when we only have one internal namespace in the module. Other larger examples
# will declare internal namespaces still
ns <- shiny::NS(id)
# The dropdown function from shiny
shiny::selectizeInput(
ns(id), # we also could've set this up by just doing shiny::NS(id, "selections")
shiny::h5(label), # here we use the label argument to title our menu
choices = NULL, # chioces are set to null because they will be filled later for what we need
options = list(allowEmptyOption = FALSE) # makes it so we don't have a blank option
)
}
# Now we set up the server end
# again we have our id argument, but we also have our
# choice_list which we can use to update our menu options
dropdown_server <- function(id, choice_list) {
# the usual call to moduleServer()
shiny::moduleServer(
id,
function (input, output, session) {
# We will update our choices here and set
# the selected to be the top choice in our
# list by default
shiny::updateSelectizeInput(
session,
id,
choices = choice_list,
selected = choice_list[[1]][1]
)
# creating an empty list of reactive values that can be filled
myreturn <- shiny::reactiveValues()
# set up the return to txt in a reactive container
shiny::observe({
myreturn$txt <- shiny::req(eval(parse(text = paste0("input$", id))))
})
return(myreturn)
})
}
# Finally, let's test it out
dropdown_demo <- function() {
# Set our UI
ui <- shiny::fluidPage(
# Call our module UI
dropdown_ui(
id = "select",
label = shiny::h5("Select an option:")
),
# Let's create a text output to test our
# module
shiny::textOutput(
'select_text'
)
)
# server
server <- function(input, output, session) {
# Call our server and set the choices list
select <- dropdown_server(
id = "select",
choice_list = c(
"A",
"B",
"C"
)
)
# Render our text output, which remember was stored in txt
output$select_text <- shiny::renderText(select$txt)
}
shinyApp(ui, server)
}
dropdown_demo()
Alternatively, we can code it in a way that better reflects mastering shiny’s documentation:
dropdown_ui <- function(id, label) {
shiny::selectizeInput(
shiny::NS(id, "select"), # changed the id to the mastering shiny method
shiny::h5(label),
choices = NULL,
options = list(allowEmptyOption = FALSE)
)
}
dropdown_server <- function(id, choice_list) {
shiny::moduleServer(
id,
function (input, output, session) {
shiny::updateSelectizeInput(
session,
id,
choices = choice_list,
selected = choice_list[[1]][1]
)
# We changed this to use a reactive container and call the select
# input we set in the UI. It knows that we are updating this input
# due to the id argument.
shiny::reactive(input$select)
})
}
# Finally, let's test it out
dropdown_demo <- function() {
ui <- shiny::fluidPage(
dropdown_ui(
id = "drop1",
label = shiny::h5("Select an option:")
),
shiny::textOutput(
'select_text'
)
)
# server
server <- function(input, output, session) {
letter <- dropdown_server(
id = "drop1",
choice_list = c(
"A",
"B",
"C"
)
)
# we call the actual value using letter() to fill our text output
# unlike the previous method
output$select_text <- shiny::renderText(letter())
}
shinyApp(ui, server)
}
dropdown_demo()
NOTE
The alternative approach improves on the code in three main ways:
- By using
NS(id, "id_name")
instead we create our internal namespace right away, whereas the original method requires tricky code at the end to link the two togethermyreturn$txt <- shiny::req(eval(parse(text = paste0("input$", id))))
- As this module only returns one value we didn’t need to return it in list format, for this reason it was simpler to call the modules return value with
letter()
as opposed toselect$txt
- Finally, the typical approach to calling values is by using
value()
versus the reactive call withvalue
, the original doesn’t follow this approach
5.3.2 CSV Download Module
Next let’s look at a module that allows for a CSV to be downloaded:
# UI set up, the typical id argument and we
# add a label argument
csv_download_ui <- function(id, label) {
# Alternative method, could use the standard if we wanted
ns <- shiny::NS(id)
# for a download we just use a button
shiny::downloadButton(
outputId = ns('download'), # setting the id
label = label, # our download title
icon = shiny::icon("download") # add in a download icon
)
}
# server setup, two arguments: one for the id, and one for
# the data that will be downloaded
csv_download_serv <- function(id, data_name) {
# the standard copy and paste start
shiny::moduleServer(
id,
function(input, output, session){
# here is our download handler
output$download <- shiny::downloadHandler(
# this is the format for the download handler
filename = function() {
# set up our file name based on the argument
paste0(data_name, ".csv")
},
# this is the format for the download handler as well
content = function(file) {
# Write the dataset to the `file` that will be downloaded
write.csv(eval(parse(text = data_name)), file)
}
)
})
}
# test ------------------------------------------------------------
download_demo <- function() {
ui <- shiny::fluidPage(
csv_download_ui(id = "download", label = "Download csv")
)
# server
server <- function(input, output, session) {
csv_download_serv(id = "download", data_name = "mtcars")
}
shinyApp(ui, server)
}
download_demo()
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()