Demystifying R Shiny Modules Through a Bigfoot Sightings App Transformation

When I discovered Shiny years ago, I got immediately hooked. Shiny is an R package to build interactive web applications that can run R code in the backend. I was fascinated by the ability it provided me to create quick data analysis and modeling prototypes that I could share with my stakeholders and get them on board. One such example is this Anomalies detection dashboard that I created to help my stakeholders understand my approach to the analysis and bring them along the journey before we moved this approach to production.
In this article, I will walk you through how you can learn to build Shiny using a modular approach, especially if you learned it the monolith way. I will work with a simple bigfoot sightings application, built on a synthetic dataset where the user can select a state and see the top 10 counties with bigfoot sightings and sightings over time. What is the business scenario? Well, I am a bigfoot enthusiast and I would like to know which county I should move to next, hence a very critical Dashboard.

Why are we talking about Shiny modules?
When I learned Shiny, I did it the monolith way, like most people. In this approach there is a ui
which contains the frontend layout components of the app, and a server
which contains the backend functions to create the app. At the time, I was making simple applications with a small app.R file, where it was easy to keep track of all ui
and server
components within the file. However, when I started writing real-world applications, my app.R file became huge very quickly and I found the following aspects to be difficult about this approach:
- Too complex to keep track of components
- Challenging to debug if something fails
- Harder for someone new to come in, learn and contribute
- Tedious to reuse its elements within itself and other applications without copy-pasting a bunch
Then I learned about the existence of Shiny modules and with some effort completely switched my approach. Think of Shiny modules as if R functions and R Shiny had babies which turned out to be very productive and efficient. In this approach, you break down the ui
and server
components, create smaller shiny application functions for those components and then call those functions from the main app.R file. So, let’s look at what that means for my bigfoot application example.
Code and data availability
Code for recreating everything in this article can be found at https://github.com/deepshamenghani/Demystifying_Shiny_modules.
Data: This article uses synthetic data with fake Bigfoot sighting locations generated by me. The synthetic data is available at the linked GitHub page and was inspired by the data I found on data.world, which was originally sourced from true sightings at https://www.bfro.net/.
Data generation process: To generate the data, I found this great article on TDS that introduced me to the faker
library. In addition to this I used the geo components of this library to generate latitude and longitude for sightings. I then used the geopy
library to get the county and state associated with the generated locations. The full code to generate this dataset can be found at the linked repo within the data folder.
The modular approach
Step 1: Decompose your application into individual components.

In my application, there are three components:
- A dropdown input filter to select the state: The dropdown is created in the
ui
side, and data is filtered based on the selected state on theserver
side. - The county plot: The space for the plot is created in the
ui
, and the ggplot2 code is written on theserver
side. - The yearly sightings plot: Similar to the county plot, the space for the plot is created in the
ui
, and the ggplot2 code is written on theserver
side.
This is what the breakdown looks like in my ui
and server
code for these components.

Step 2: Create the modules for the decomposed elements
Now that I have recognized three components, I will create a module for each of these.
Module 1: Module_input.R
Let’s look at the code for writing the first module – Module_input.R, which is a Shiny module to create the dropdown and filter data based on the dropdown input.
# UI module function
module_input_ui <- function(id, df, defaultstate = "California") {
# Namespace
ns <- NS(id)
# Input UI command
selectizeInput(inputId = ns("stateinput"), label = "Select state",
choices = unique(df$state),
selected = defaultstate, multiple = FALSE)
}
# Server module function
module_input_server <- function(id, df) {
# Server function to filter the data based on state input
moduleServer(id,
function(input, output, session) {
# Filter data set based on input
table <- reactive({df |> filter(state == input$stateinput)})
return(table)
})
}
There are some primary similarities between the monolith app and this shiny module:
- Just like the original Shiny app, this Shiny module has a
ui
andserver
components. - The code to create the dropdown sits in the
ui
, and the code to filter data based on the dropdown input by user sits in theserver
side.
Now, let’s look at the differences:
- Functions: In Shiny module, the
ui
andserver
are both written as functions. This is because these functions will be instantiated from the main app.R file to then create the dropdown and filtering instead of directly writing the dropdown and filtering code. - ID: The first input into these two functions is the id. Think of
id
through the lens of a blind date, as if bothui
andserver
function calls are holding a red rose to find each other in a crowded space. When the functions are called from the main app.R file, this unique id tells theserver
function call whichui
function call to get the dropdown state input from. It also tells theui
function call whichserver
function call should it provide the state input to for the data filtering. This is extremely important, especially when you make multiple function calls to various Shiny modules and you always want the ui calls and corresponding server calls to talk to each other correctly. - Namespace: As it suggests, it is a space for the variable names associated with the unique id. When you put variable names inside a namespace, they don’t conflict with other similar variable names that might be used in other shiny module function calls. This is the key that differentiates Shiny modules from base functions. This also allows multiple people to work on different modules of the application without worrying about conflicts. A variable name associated with one unique id will not conflict with the same variable name associated with a different unique id.
Now I will move on to the code for the other two modules. I will skip the explanation as they are created with a similar structure as the first module.
Module 2: Module_countyplot.R
# UI module function
module_county_ui <- function(id) {
# Namespace
ns <- NS(id)
plotOutput(outputId = ns("plotcounty"))
}
# Server module function
module_county_server <- function(id, df_filtered) {
moduleServer(id,
function(input, output, session) {
# County plot code
output$plotcounty <- renderPlot({
df_filtered() |>
count(county) |>
ggplot() +
geom_col(aes(county, n, fill = n),
colour = NA, width = 0.8)
# ... rest of the plot code
})
})
}
Module 3: Module_yearlyplot.R
# UI module function
module_yearly_ui <- function(id) {
# Namespace
ns <- NS(id)
plotOutput(outputId = ns("plotyearly"))
}
# Server module function
module_yearly_server <- function(id, df_filtered) {
moduleServer(id,
function(input, output, session) {
# Yearly plot code
output$plotyearly <- renderPlot({
df_filtered() |>
count(year) |>
ggplot(aes(year, n)) +
geom_point(aes(year, highest_count), alpha=1, size = 2)
# ... rest of the plot code
})
})
}
Step 3: Test the modules
Each Shiny module should be successful when called from the main app.R file. Let’s test the first module by creating the main app.R file, which will call the ui
and server
functions from the first module file. This should create a very basic shiny app that only showcases the state dropdown filter and then filters the data based upon that, as this is all that the first module does.
# Source the module.R file created for the ui and server functions of dropdown
source("./modules/module_input.R")
dataset <- read.csv("./data/bfro_reports_geocoded.csv")
ui_test <- fluidPage(
# Call the ui module as a function
module_input_ui("input_test", df=dataset)
)
server_test <- function(input, output, session) {
# Call the server module as a function
data_filtered <- module_input_server("input_test", df=dataset)
}
# Call the shiny app
shinyApp(ui=ui_test, server=server_test)
This should create the following basic application.

There are two things to note here. First, the original code for dropdown and data filtering has been replaced with the module function calls for the ui
and server
. Second, both ui
and server
function calls have the same "input_test
" id, which enables them to interact with each other.
The other two modules are tested similarly to ensure they work as individual shiny applications and the function calls are successful.
Step 4: Turn the original app.R to a modularised app.R file
Now, I will expand on the previous step and call all the modules from my app.R file to update the code for my original bigfoot application.
# Source the three module.R files to access their ui and server functions
source("./modules/module_input.R")
source("./modules/module_countyplot.R")
source("./modules/module_yearlyplot.R")
dataset <- read.csv("./data/bfro_reports_geocoded.csv")
ui <- fluidPage(
h1("Bigfoot Sightings in the United States", align="center"),
fluidRow(
# ui function call for state dropdown
column(2, module_input_ui("inputs", dataset)),
# ui function call for county plot
column(5, module_county_ui("countyplot")),
# ui function call for yearly plot
column(5, module_yearly_ui("timeplot"))
)
)
# Define server logic ----
server <- function(input, output, session) {
# Server function call for data filtering
data_filtered <- module_input_server("inputs", dataset)
# Server function call for county plot code
module_county_server("countyplot", df_filtered = data_filtered)
# Server function call for yearly plot
module_yearly_server("timeplot", df_filtered = data_filtered)
}
# Run the application
shinyApp(ui = ui, server = server)
That is it, the original app now has been updated to a modular Shiny application.
Advantages of the modular approach

Now that the app has been modularized with the approach pictured here, all the setbacks of the monolith Shiny app that I shared earlier are resolved because:
- The code is easy to read as each space is designated to a function call.
- If a part of the Shiny application fails, then I can go to only that module, correct the code, and test the module instead of finding the point of failure in a long code file and rerunning the entire application to debug.
- Anybody can read this application more easily and contribute independently without worrying about conflicts.
- Parts of the application can be reused. For instance, if I wanted to have multiple dropdowns and county plots to compare between states, then I can call those module functions multiple times with different unique
ids
, instead of copy-pasting code.
Next steps
If you are new to Shiny, I highly recommend directly learning to build it with a modular approach. While it takes a bit longer to start with this, it does create a solid foundation for larger applications in the future.
If like me, you learned it the monolith way, it is a great idea to switch to a modular approach as you build more applications. Check out the Github repo for modular and non-modular bigfoot app code to understand how you can make that switch.
Lastly, enjoy a great read of Mastering Shiny book by Hadley Wickham which thoroughly goes through the foundational concepts of building Shiny.
If you want to learn more about my approach of going modular, check out this talk I gave at Cascadia R conference. I hope you find it useful and convert to a modular approach for your future Shiny applications.
Code for recreating everything in this article can be found at https://github.com/deepshamenghani/Demystifying_Shiny_modules.
If you’d like, find me on Linkedin.