Skip to Content
Course content

87: Shiny Modules for Reusable Components

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started building complex Shiny apps, I fell into a trap that almost every R developer hits: I thought Shiny Modules were just a fancy way to organize my code. I treated them like folders for my functions—a way to stop my app.R file from becoming a 2,000-line monster. If that's how you're thinking about them, you're missing the entire point of the feature.

Modules aren't for organization; they're for namespace isolation

Here is the concrete problem: Imagine you've built a perfect "Data Filter" UI block with a date range picker and a dropdown for categories. It works great. Now, your boss asks you to add a second, identical filter block for a different dataset on the same page.

If you just copy-paste your UI and server code, your app will break. Why? Because you'll have two inputs both named input$date_range. Shiny has no way of knowing which one you're referring to in your server logic. You'd be forced to manually rename every single ID to input$date_range_1 and input$date_range_2. It's tedious, error-prone, and a total nightmare to maintain.

Modules solve this by creating a "namespace." A module is a self-contained unit that wraps its IDs. When you call a module multiple times, Shiny automatically prefixes the IDs behind the scenes, so date_range becomes filter1-date_range and filter2-date_range without you ever having to type those prefixes manually.

The Pattern: UI and Server Pairs

To make this work, a module consists of two parts: a UI function and a server function. I always recommend naming them with a consistent suffix like UI and Server to keep your sanity.

# The UI part of the module
plotModuleUI <- function(id) {
  ns <- NS(id) # This is the magic sauce
  tagList(
    selectInput(ns("variable"), "Choose Variable", choices = c("mpg", "hp", "wt")),
    plotOutput(ns("distPlot"))
  )
}

# The Server part of the module
plotModuleServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    output$distPlot <- renderPlot({
      hist(mtcars[[input$variable]], main = "Module Plot")
    })
  })
}

Notice the ns() function in the UI. That stands for "namespace." Every single ID inside the module's UI must be wrapped in ns(). If you forget one, that specific input will look for its value in the global app scope instead of the module scope, and you'll spend an hour wondering why your reactive values are NULL. I've been there; don't be there.

Plugging Modules into the Main App

Now that we have our module, we can call it as many times as we want in the main app. We just provide a unique string as the id for each instance.

ui <- fluidPage(
  plotModuleUI("plot_a"),
  plotModuleUI("plot_b")
)

server <- function(input, output, session) {
  # We call the server logic for each instance
  plotModuleServer("plot_a")
  plotModuleServer("plot_b")
}

In this setup, plot_a and plot_b are completely independent. Changing the dropdown in the first plot won't affect the second. You've essentially created a reusable "component" that you can drop into any app you build from now on.

Passing Data Between Modules and the Main App

You'll eventually realize that modules shouldn't be totally isolated; they often need to talk to the main app. The best way to handle this is to have the module's server function return a reactive value.

If my plotModuleServer returned return(reactive(input$variable)), the main app could then capture that value: selected_var <- plotModuleServer("plot_a"). This keeps the module generic while allowing the main app to coordinate the high-level logic.




📋 Practical Task

Exercise: Building a Reusable KPI Metric Card

Your goal is to create a Shiny app that displays three different KPI cards (e.g., "Total Revenue", "Active Users", and "Conversion Rate"). Instead of writing the UI and server logic three times, you will build a single kpiCard module.

Requirements:

  • Create a kpiCardUI function that takes an id and a label. It should contain:
    • A heading showing the label.
    • A numericInput (wrapped in ns()) to allow the user to manually update the value.
    • A textOutput (wrapped in ns()) that displays the value with a prefix like "Current Value: ".
  • Create a kpiCardServer function that handles the logic to render the text output based on the numeric input.
  • In the main ui, instantiate the kpiCardUI three times with different labels: "Revenue", "Users", and "Conversion".
  • In the main server, call the kpiCardServer three times using matching IDs.

Success Criteria: You should be able to change the number in the "Revenue" card without the "Users" or "Conversion" cards updating. This proves your namespaces are isolated correctly.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.