0Pricing
R Academy · Lesson

Shiny Modules for Code Reuse

Encapsulate UI and server logic into namespaced, reusable modules.

Shiny Modules for Code Reuse is a free R Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Shiny Modules?

As Shiny apps grow, having all UI and server code in one file becomes unmanageable. Modules are self-contained pieces of Shiny UI + server logic with namespaced IDs. You can reuse the same module multiple times in one app without ID conflicts, and test modules independently.

# Problem: without modules, ID conflicts arise
# ui <- fluidPage(
#   selectInput('dataset', ...),  # used by plot1 AND plot2!
#   selectInput('dataset', ...)   # duplicate ID — BROKEN
# )

# With modules: each instance has its own namespaced IDs
# plotModule('plot1', ...)  ->  input$plot1-dataset
# plotModule('plot2', ...)  ->  input$plot2-dataset

NS() — The Namespace Function

Every module UI function starts with ns <- NS(id). All UI element IDs passed to Shiny must be wrapped in ns(). This prepends id- to each ID, creating a namespace that prevents conflicts between module instances.

# Module UI function
filter_plot_ui <- function(id) {
  ns <- NS(id)  # create the namespace function

  tagList(
    selectInput(ns('dataset'), 'Choose Dataset:',
                choices = c('mtcars', 'iris', 'airquality')),
    sliderInput(ns('n_rows'), 'Rows to show:', 1, 50, 20),
    plotOutput(ns('scatter_plot'))
  )
}

moduleServer() — The Server Function

moduleServer(id, function(input, output, session) {...}) is the modern (Shiny 1.5+) way to define module server logic. Inside the function, input, output, and session are automatically namespaced — you access input$dataset not input$plot1-dataset.

# Module server function
filter_plot_server <- function(id) {
  moduleServer(id, function(input, output, session) {

    data <- reactive({
      # input$dataset is already namespaced to this instance
      head(get(input$dataset), input$n_rows)
    })

    output$scatter_plot <- renderPlot({
      df <- data()
      plot(df[[1]], df[[2]],
           xlab = names(df)[1], ylab = names(df)[2])
    })
  })
}

Using Modules in the App

Call the module UI function in ui and the module server function in server, both using the same id string. You can call the same module multiple times with different IDs to create independent instances.

# Main app using the module twice
ui <- fluidPage(
  h2('Plot 1'),
  filter_plot_ui('plot1'),   # instance 1
  hr(),
  h2('Plot 2'),
  filter_plot_ui('plot2')    # instance 2 — no ID conflicts!
)

server <- function(input, output, session) {
  filter_plot_server('plot1')  # wire up instance 1
  filter_plot_server('plot2')  # wire up instance 2
}

shinyApp(ui, server)

Passing Parameters to Module UI

Module UI functions are just regular R functions. Add extra parameters beyond id to customise each instance's appearance or behaviour at creation time. These are evaluated once when the UI is built.

# Module UI with extra parameters
summary_table_ui <- function(id, title = 'Summary', height = '300px') {
  ns <- NS(id)
  tagList(
    h4(title),
    div(
      style = paste0('height:', height, '; overflow-y: auto;'),
      DTOutput(ns('tbl'))
    )
  )
}

# Use with custom titles
summary_table_ui('train_tbl', title = 'Training Data', height = '400px')
summary_table_ui('test_tbl',  title = 'Test Data',     height = '200px')

Passing Reactives INTO a Module

Module server functions can accept reactive values or expressions as parameters. This allows the parent app to pass data down to child modules. Inside the module, call the reactive like a function to get its current value.

# Module that accepts a reactive as input
chart_module_server <- function(id, data_reactive) {
  moduleServer(id, function(input, output, session) {
    output$chart <- renderPlot({
      df <- data_reactive()   # call the reactive passed in
      ggplot2::ggplot(df, ggplot2::aes(x = x, y = y)) +
        ggplot2::geom_point(colour = input$colour)
    })
  })
}

# In main server:
server <- function(input, output, session) {
  shared_data <- reactive({ load_data(input$source) })
  chart_module_server('chart1', data_reactive = shared_data)
  chart_module_server('chart2', data_reactive = shared_data)
}

Returning Reactives FROM a Module

Module server functions can return reactive values to the parent. This allows child modules to communicate upward. Return a reactive or a list of reactives from moduleServer() and capture it in the parent server function.

# Module that returns a reactive to the parent
filter_module_server <- function(id, all_data) {
  moduleServer(id, function(input, output, session) {

    # Return the filtered data reactive
    filtered <- reactive({
      all_data[all_data$group == input$group_filter, ]
    })

    return(filtered)  # parent can use this reactive
  })
}

# In main server:
server <- function(input, output, session) {
  raw_data <- reactive({ read.csv('data.csv') })

  # filtered_data is a reactive returned from the module
  filtered_data <- filter_module_server('filter1', raw_data)

  output$main_plot <- renderPlot({ plot(filtered_data()) })
}

Module File Organisation

For large apps, place each module in its own file in an R/ folder. Shiny automatically sources all files in R/ when the app loads. This keeps each module self-contained and testable independently using the shinytest2 package.

# Recommended project structure:
# myapp/
#   app.R                   # main app: source modules + wire up
#   R/
#     mod_filter_plot.R     # filter_plot_ui() + filter_plot_server()
#     mod_summary_table.R   # summary_table_ui() + summary_table_server()
#     mod_download.R        # download_ui() + download_server()
#   tests/
#     testthat/test-mod_filter_plot.R

# In app.R:
library(shiny)
# source('R/mod_filter_plot.R')  # not needed if in R/ folder
ui     <- fluidPage(filter_plot_ui('p1'))
server <- function(input, output, session) { filter_plot_server('p1') }
shinyApp(ui, server)

Nested Modules

Modules can contain other modules. The parent module passes its own session namespace down to child module calls using the session argument. Each level of nesting adds another namespace prefix: outer-inner-element_id.

# Outer module uses an inner module
outer_server <- function(id) {
  moduleServer(id, function(input, output, session) {

    # Call an inner module using this module's session
    inner_result <- inner_module_server(
      id      = 'inner',
      session = session  # passes the namespaced session
    )

    output$combined <- renderText({
      paste('Inner result:', inner_result())
    })
  })
}

# ID chain: outer -> inner
# Full ID: outer-inner-element

Testing Modules with shinytest2

The shinytest2 package lets you write automated tests for modules by wrapping them in a minimal app. Use AppDriver to drive the browser, set inputs, and assert output values — all without a real browser session.

library(shinytest2)

# Wrap the module in a testable app
test_that('filter_plot module filters correctly', {
  test_app <- shinyApp(
    ui     = fluidPage(filter_plot_ui('test')),
    server = function(input, output, session) {
      filter_plot_server('test')
    }
  )

  app <- AppDriver$new(test_app)
  app$set_inputs('test-dataset' = 'iris')  # namespaced input
  app$wait_for_idle()

  # Assert plot was rendered
  expect_true(!is.null(app$get_value(output = 'test-scatter_plot')))
})

Module Communication Patterns

A summary of communication patterns between modules and the parent app:

  • Parent to Module: pass reactive as parameter to module server.
  • Module to Parent: return reactive from moduleServer().
  • Between Sibling Modules: parent holds shared state (reactiveValues) and passes it to each module.
  • Global State: use reactiveValues defined in the parent and pass references down.
# Sibling module communication via parent state
server <- function(input, output, session) {
  shared <- reactiveValues(selected_row = NULL)

  # Table module sets the selection
  table_module_server('tbl', shared_state = shared)

  # Detail module reads the selection
  detail_module_server('detail', shared_state = shared)

  # Both modules communicate through 'shared' reactiveValues
  # Table sets shared$selected_row; Detail reads it
}

Quick Check

Why must all UI element IDs in a module UI function be wrapped with ns()?

Shiny Modules Recap

Key takeaways from Shiny Modules for Code Reuse:

  • Modules prevent ID conflicts by namespacing all IDs with NS(id).
  • Module UI: regular function with ns <- NS(id); wrap all IDs with ns().
  • Module server: moduleServer(id, function(input, output, session) {...}).
  • Pass reactives INTO modules as function parameters; RETURN reactives for upward communication.
  • Call the same module multiple times with different IDs for independent instances.
  • Organise modules in R/mod_*.R files; Shiny auto-sources the R/ folder.
  • Test modules with shinytest2::AppDriver.
# Complete module example
my_module_ui <- function(id) {
  ns <- NS(id)
  tagList(selectInput(ns('var'), 'Variable:', choices = names(mtcars)),
          plotOutput(ns('hist')))
}

my_module_server <- function(id, data) {
  moduleServer(id, function(input, output, session) {
    output$hist <- renderPlot(hist(data()[[input$var]]))
  })
}

# Use it:
ui <- fluidPage(my_module_ui('m1'), my_module_ui('m2'))
server <- function(input, output, session) {
  d <- reactive(mtcars)
  my_module_server('m1', d)
  my_module_server('m2', d)
}
shinyApp(ui, server)

Frequently asked questions

Is the “Shiny Modules for Code Reuse” lesson free?

Yes — the full text of “Shiny Modules for Code Reuse” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.

What will I learn in “Shiny Modules for Code Reuse”?

Encapsulate UI and server logic into namespaced, reusable modules. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Shiny Modules for Code Reuse” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Reactive Programming Deep Dive
  2. Shiny Modules for Code Reuse
  3. Dynamic UI with renderUI and insertUI
  4. Deploying Shiny Apps
← Back to R Academy