The world’s leading publication for data science, AI, and ML professionals.

Demystifying Shiny Modules by Transforming a Bigfoot Sightings App Modular

In-depth guide to learning how to build Shiny applications using modules.

Demystifying R Shiny Modules Through a Bigfoot Sightings App Transformation

Photo by Luke Chesser on Unsplash
Photo by Luke Chesser on Unsplash

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.

Image by author
Image by author

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.

Image by author
Image by author

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 the server side.
  • The county plot: The space for the plot is created in the ui, and the ggplot2 code is written on the server 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 the server side.

This is what the breakdown looks like in my ui and server code for these components.

Image by author
Image by author

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:

  1. Just like the original Shiny app, this Shiny module has a ui and server components.
  2. 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 the server side.

Now, let’s look at the differences:

  1. Functions: In Shiny module, the ui and server 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.
  2. ID: The first input into these two functions is the id. Think of id through the lens of a blind date, as if both ui and server 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 the server function call which ui function call to get the dropdown state input from. It also tells the ui function call which server 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.
  3. 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.

Image by author
Image by author

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

Image by author
Image by author

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:

  1. The code is easy to read as each space is designated to a function call.
  2. 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.
  3. Anybody can read this application more easily and contribute independently without worrying about conflicts.
  4. 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.


Related Articles