R Shiny Modules: How to Create Your First R Shiny Module

Reading time:
time
min
By:
Dario Radečić
November 8, 2023

Let's be honest - all of your R Shiny app logic is contained in a single <code>app.R</code> file, and it's probably several hundreds of lines long. No judgment - we've all been there. But there's a better way, and today you'll learn all about it.

R Shiny modules provide you with the ability to separate reusable logic into small and manageable R scripts. You can then plug different modules into your <code>app.R</code> and end up with a professional-looking code base. Think of R Shiny modules as components in React.
<blockquote>Are you new to R Shiny? <a href="https://appsilon.com/how-to-start-a-career-as-an-r-shiny-developer/" target="_blank" rel="noopener">This article will explain how to make a career out of it.</a></blockquote>
Table of contents:
<ul><li><a href="#introduction">What are R Shiny Modules and Why Should You Care?</a></li><li><a href="#intro-to-modules">Introduction to Modules in R Shiny - Create Your First Module</a></li><li><a href="#multiple-modules">Combining Multiple R Shiny Modules - Let's Build an App</a></li><li><a href="#summary">Summing up R Shiny Modules</a></li></ul>

<hr />

<h2 id="introduction">What are R Shiny Modules and Why Should You Care?</h2>
People often think of R Shiny modules as an advanced topic, but that doesn't have to be the case. Sure, modules can sometimes make even an experienced R programmer uncomfortable, but there are plenty of easy topics to explore.

In a nutshell, <b>if you understand R Shiny basics and know how to write R functions, you have everything needed to start learning about R Shiny modules</b>. Yup, it's that simple!

As we mentioned earlier, most beginners tend to use only one file - <code>app.R</code> when structuring Shiny apps, which, needless to say, results in a big chunk of messy code.

Shiny modules act like functions by wrapping up sets of UI and server elements. They will ensure a modular structure with reusable components and no code duplication.

If you understand the <b>DRY (<a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" target="_blank" rel="noopener noreferrer">Don't Repeat Yourself</a>)</b> concept of programming, you'll understand why modularizing an application might be a good thing to do.

You can also think of R Shiny modules as many small Shiny applications that are composed later in a single application file.

Okay, so now you know the basics. Up next, you'll see how to create your first module.
<h2 id="intro-to-modules">Introduction to Modules in R Shiny - Create Your First Module</h2>
In this section, you'll learn how to make a module responsible for plotting a bar chart. It's a simple one, sure, but will enable you to see how everything connects.

We recommend you create a new directory and two files inside it - <code>app.R</code> and <code>mod-chart.R</code>. The latter will contain all of the module logic, while the prior will use the module in a Shiny application.
<h3>Writing the Code for a Custom Chart Module</h3>
Let's first discuss <code>mod-chart.R</code>. You will need to write two functions, one for the module UI and the other for the module server.

The <b>UI function</b> can use your standard Shiny functions, just like you would typically do with a single file app structure. In our case, we'll use the <code>fluidRow()</code> and <code>plotOutput()</code> function to indicate the UI element will take up the entire row and will render a chart.

The other important thing about the UI is the <code>NS()</code> function. Always use it with custom modules, as it will ensure a unique variable name for your input/output elements (IDs).

On the other hand, the <code>server function</code> can take up as many arguments as you want. Make it as customizable as you need. We decided to make the plot have variables X, Y, and title values.

In the server function, you'll need to use the <code>moduleServer()</code> function to specify the module ID and its function. This inner function basically does what your regular R Shiny <code>server()</code> function does - deals with reactive values and renders elements.

Anyhow, here's the full code snippet for the <code>mod-chart.R</code> file:
<pre><code class="language-r">library(ggplot2)
library(shiny)
<br>
chartUI &lt;- function(id) {
 # Create unique variable name
 ns &lt;- NS(id)
 fluidRow(
   plotOutput(outputId = ns("chart"))
 )
}
<br>
chartServer &lt;- function(id, x, y, title) {
 moduleServer(
   id = id,
   module = function(input, output, session) {
     # Convert input data to data.frame
     df &lt;- reactive({
       data.frame(
         x = x,
         y = y
       )
     })
<br>      # Render a plot
     output$chart &lt;- renderPlot({
       ggplot(df(), aes(x = x, y = y)) +
         geom_col() +
         labs(
           title = title
         )
     })
   }
 )
}</code></pre>
Sure, there are some module-specific functions you need to learn about, but everything else should look familiar. Let's now use this file in <code>app.R</code>
<h3>Using the Custom Chart Module in R Shiny</h3>
Let's now switch to <code>app.R</code> and import our newly created module by calling <code>source("mod-chart.R")</code> function.

From there, it's your everyday R Shiny application. We can leverage the <code>chartUI()</code> and <code>chartServer()</code> functions to render the contents of our module, and the code logic looks exactly the same as if you were to use a set of built-in Shiny functions.

Just note the parameters required for <code>chartServer()</code> - These are the ones declared as arguments to the function in the module file.

Here's the full code snippet:
<pre><code class="language-r">library(shiny)
source("mod-chart.R")
<br>
ui &lt;- fluidPage(
 chartUI(id = "chart1")
)
<br>
server &lt;- function(input, output, session) {
 chartServer(
   id = "chart1",
   x = c("Q1", "Q2", "Q3", "Q4"),
   y = c(505.21, 397.18, 591.44, 674.90),
   title = "Sales in 000 for 2023"
 )
}
<br>
shinyApp(ui = ui, server = server)</code></pre>
You can now run the app to see the chart:

<img class="size-full wp-image-20654" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b7ae33ef0ba20db0446654_539ba730_1.webp" alt="Image 1 - Modularized R Shiny chart" width="1704" height="796" /> Image 1 - Modularized R Shiny chart

That was easy, wasn't it? Let's give the chart a bit of a visual overhaul next.
<h3>Additional Module Tweaking</h3>
This section is optional to follow, but will somewhat improve the aesthetics of our chart. The module logic inside <code>mod-chart.R</code> remains the same - we're just adding a couple more <code>ggplot2</code> functions to the chart to make it look nicer.

Take a look for yourself:
<pre><code class="language-r">library(ggplot2)
library(shiny)
<br>
chartUI &lt;- function(id) {
 # Create unique variable name
 ns &lt;- NS(id)
 fluidRow(
   plotOutput(outputId = ns("chart"))
 )
}
<br>
chartServer &lt;- function(id, x, y, title) {
 moduleServer(
   id = id,
   module = function(input, output, session) {
     # Convert input data to data.frame
     df &lt;- reactive({
       data.frame(
         x = x,
         y = y
       )
     })
<br>      # Render a plot
     output$chart &lt;- renderPlot({
       ggplot(df(), aes(x = x, y = y)) +
         geom_col(fill = "#0099f9") +
         geom_text(aes(label = y), vjust = 2, size = 6, color = "#ffffff") +
         labs(
           title = title
         ) +
         theme_classic() +
         theme(
           plot.title = element_text(hjust = 0.5, size = 20, face = "bold"),
           axis.title.x = element_text(size = 15),
           axis.title.y = element_text(size = 15),
           axis.text.x = element_text(size = 12),
           axis.text.y = element_text(size = 12)
         )
     })
   }
 )
}</code></pre>
In a nutshell, we've changed the bar color, given text labels inside the bars, tweaked the overall theme, centered the title, and increased font sizes for axis labels and ticks.

You can restart the app now - here's what you will see:

<img class="size-full wp-image-20656" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b7ae3392ce905dbe61e1a5_21f891d0_2.webp" alt="Image 2 - Styled ggplot2 chart" width="1704" height="796" /> Image 2 - Styled ggplot2 chart

Let's now discuss what really matters with R Shiny modules - reusability.
<blockquote>Want to learn more about bar charts in R? <a href="https://appsilon.com/ggplot2-bar-charts/" target="_blank" rel="noopener">Read our complete guide to gpplot2</a>.</blockquote>
<h3>Reusing the Module in R Shiny</h3>
Since the UI and server logic of our module are basically R functions, you can call them however many times you want in your R Shiny app.

The example below creates a second chart with our module and shows you how effortless this task is now. No need to repeat the logic - simply call the functions and you're good to go:
<pre><code class="language-r">library(shiny)
source("mod-chart.R")
<br>
ui &lt;- fluidPage(
 chartUI(id = "chart1"),
 chartUI(id = "chart2")
)
<br>
server &lt;- function(input, output, session) {
 chartServer(
   id = "chart1",
   x = c("Q1", "Q2", "Q3", "Q4"),
   y = c(505.21, 397.18, 591.44, 674.90),
   title = "Sales in 000 for 2023"
 )
 chartServer(
   id = "chart2",
   x = c("IT", "Sales", "Marketing", "HR"),
   y = c(15, 23, 19, 5),
   title = "Number of employees per department"
 )
}
<br>
shinyApp(ui = ui, server = server)</code></pre>
Here's what you'll see when you restart the app:

Image 3 - Two stacked modularized charts

You now know how to create R Shiny modules, so next we'll kick things up a notch by combining multiple modules in a single Shiny application.
<h2 id="multiple-modules">Combining Multiple R Shiny Modules - Let's Build an App</h2>
If you've followed through with the previous section, this one will feel like a walk in the park.

Essentially, we'll build an R Shiny application based on the <a href="https://appsilon.com/r-dplyr-gapminder/" target="_blank" rel="noopener">Gapminder dataset.</a> The app will allow the user to select a continent, and the contents of the app will automatically update to show:
<ul><li>Year, average life expectancy, and average GDP per capita as a data table.</li><li>Average life expectancy over time as a bar chart.</li><li>Average GDP per capita as a line chart.</li></ul>
To achieve this, we'll write another new module and update the existing one to accommodate more chart types. Let's dig in!
<h3>Writing the Custom Table Module</h3>
First things first, let's discuss the new data table module. Create a new file named  <code>mod-table.R</code>, and declare two functions for taking care of UI and server - <code>tableUI()</code> and <code>tableServer()</code>.

The UI function will use DT to create a new table, and the server function will render the table based on the passed <code>data.frame</code>, column names, and table captions.
<blockquote>Want to learn more about R packages for visualizing table data? <a href="https://appsilon.com/top-r-packages-for-table-data/" target="_blank" rel="noopener">These packages are everything you'll ever need</a>.</blockquote>
Here's the entire code snippet for <code>mod-table.R</code>:
<pre><code class="language-r">library(shiny)
library(DT)
<br>
tableUI &lt;- function(id) {
 # Unique variable name
 ns &lt;- NS(id)
 fluidRow(
   DTOutput(outputId = ns("table"))
 )
}
<br>
tableServer &lt;- function(id, df, colnames, caption) {
 moduleServer(
   id = id,
   module = function(input, output, session) {
     # Render a table
     output$table &lt;- renderDT({
       datatable(
         data = df(),
         colnames = colnames,
         caption = caption,
         filter = "top"
       )
     })
   }
 )
}</code></pre>
Up next, let's tweak the chart module.
<h3>Writing the Custom Chart Module</h3>
Our <code>mod-chart.R</code> file needs a bit of tweaking. We'll now allow the user to specify the chart type (only bar and line). In addition, the entire <code>data.frame</code> will now be passed alongside string names for the X and Y columns. This is only to keep congruent with the logic inside <code>mod-table.R</code>.

Both chart types will be themed the same, so it makes sense to extract the theming logic to reduce code duplication. The line chart will also be colored differently, just so we can end up with a more <i>lively</i> application.

This is the updated code for <code>mod-chart.R</code> file:
<pre><code class="language-r">library(shiny)
library(ggplot2)
<br>
chartUI &lt;- function(id) {
 # Unique variable name
 ns &lt;- NS(id)
 fluidRow(
   plotOutput(outputId = ns("chart"))
 )
}
<br>
chartServer &lt;- function(id, type, df, x_col_name, y_col_name, title) {
 moduleServer(
   id = id,
   module = function(input, output, session) {
     # Chart logic
     # Extract a common property
     chart_theme &lt;- ggplot2::theme(
       plot.title = element_text(hjust = 0.5, size = 20, face = "bold"),
       axis.title.x = element_text(size = 15),
       axis.title.y = element_text(size = 15),
       axis.text.x = element_text(size = 12),
       axis.text.y = element_text(size = 12)
     )
<br>      # Line chart
     if (type == "line") {
       output$chart &lt;- renderPlot({
         ggplot(df(), aes_string(x = x_col_name, y = y_col_name)) +
           geom_line(color = "#f96000", size = 2) +
           geom_point(color = "#f96000", size = 5) +
           geom_label(
             aes_string(label = y_col_name),
             nudge_x = 0.25,
             nudge_y = 0.25
           ) +
           labs(title = title) +
           theme_classic() +
           chart_theme
       })
<br>      # Bar chart
     } else {
       output$chart &lt;- renderPlot({
         ggplot(df(), aes_string(x = x_col_name, y = y_col_name)) +
           geom_col(fill = "#0099f9") +
           geom_text(aes_string(label = y_col_name), vjust = 2, size = 6, color = "#ffffff") +
           labs(title = title) +
           theme_classic() +
           chart_theme
       })
     }
   }
 )
}</code></pre>
Let's now use both of these in <code>app.R</code>.
<blockquote>Looking to learn more about line charts? <a href="https://appsilon.com/ggplot2-line-charts/" target="_blank" rel="noopener">This article has you covered</a>.</blockquote>
<h3>Tying it All Together</h3>
First things first - don't forget to load the table module code at the top of the R file.

Our application will use the sidebar layout to separate user controls from the app contents. The user will have access to a dropdown menu from which to continent can be changed. It will be set to Europe by default.

The main panel will call dedicated module functions - once for the table and twice for the chart.

As for the <code>server()</code> function, we'll filter and summarize the Gapminder dataset and hold it as a reactive value. Then, we'll call the module server functions to render dynamic data and pass in the required parameters:
<pre><code class="language-r">library(shiny)
library(gapminder)
source("mod-chart.R")
source("mod-table.R")
<br>
ui &lt;- fluidPage(
 sidebarLayout(
   sidebarPanel(
     tags$h3("Shiny Module Showcase"),
     tags$hr(),
     selectInput(inputId = "continent", label = "Continent:", choices = unique(gapminder$continent), selected = "Europe")
   ),
   mainPanel(
     tableUI(id = "table-data"),
     chartUI(id = "chart-bar"),
     chartUI(id = "chart-line")
   )
 )
)
<br>
server &lt;- function(input, output, session) {
 # Filter the dataset first
 data &lt;- reactive({
   gapminder %&gt;%
     filter(continent == input$continent) %&gt;%
     group_by(year) %&gt;%
     summarise(
       avg_life_exp = round(mean(lifeExp), digits = 0),
       avg_gdp_percap = round(mean(gdpPercap), digits = 2)
     )
 })
 
 # Data table
 tableServer(
   id = "table-data",
   df = data,
   colnames = c("Year", "Average life expectancy", "Average GDP per capita"),
   caption = "Gapminder datasets stats by year and continent"
 )
<br>  # Bar chart
 chartServer(
   id = "chart-bar",
   type = "bar",
   df = data,
   x_col_name = "year",
   y_col_name = "avg_life_exp",
   title = "Average life expectancy over time"
 )
<br>  # Line chart
 chartServer(
   id = "chart-line",
   type = "line",
   df = data,
   x_col_name = "year",
   y_col_name = "avg_gdp_percap",
   title = "Average GDP per capita over time"
 )
}
<br>
shinyApp(ui = ui, server = server)</code></pre>
And that's it - nice and tidy! Let's launch the app to see if everything works as expected:

<img class="size-full wp-image-20660" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b7ae3459d451e62c8822d8_21b0191c_4.gif" alt="Image 4 - Finalized R Shiny modules app" width="1160" height="704" /> Image 4 - Finalized R Shiny modules app

As you can see, combining multiple R Shiny modules is as easy as using only one of them. The code base now looks extra clean and doesn't suffer from code duplication.

<hr />

<h2 id="summary">Summing up R Shiny Modules</h2>
And there you have it - your first modularized R Shiny application. You've seen how easy it is to extract programming logic from the main application file and how good of a job this does when creating multiple objects of the same type.

For the homework assignment, we strongly recommend you try adding a third type of chart and tweaking the visuals of the data table. This will further increase the customizability of the app and will give you time to practice.

<i>What's your approach when using R Shiny modules? Are they a de facto standard even for small apps?</i> Let us know in the comment section below.
<blockquote>R and R Shiny can help you improve business workflows - <a href="https://appsilon.com/r-programming-vs-excel-for-business-workflow/" target="_blank" rel="noopener">Here are 5 examples how</a>.</blockquote>


Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!

Is Your Software GxP Compliant?

Download a checklist designed for clinical managers in data departments to make sure that software meets requirements for FDA and EMA submissions.
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
r
shiny
tutorials