Introduction: Why a Different Approach for Modules?
While link_plots()
is perfect for single-file Shiny
applications, a more robust pattern is needed when working with
Shiny Modules. This is because modules are designed to
be self-contained and reusable, a principle known as
encapsulation.
The main app should not need to know the internal
outputId
s of the components within a module. For
linkeR
to work correctly, it needs to be aware of the
unique “namespace” that Shiny gives to each module’s UI elements.
The recommended pattern for modular apps follows three simple steps that respect module encapsulation and lead to cleaner, more maintainable code.
The Three-Step Pattern for Modular Linking
The core idea is to create a central “linking manager” (the registry) in your main app and pass it down to each module. The modules then register their own components with this central manager.
-
Main App: Create a central
link_registry
object. -
Main App: Pass the
registry
object into each module’s server function. -
Inside Each Module: Call the relevant
register_xyz
function from the module’s server, using the module’s ownsession
object.
This vignette will walk through building a modular app based on the
example found in inst/examples/modularized_example/
.
Step 1: Create the Map Module (map_module.R
)
First, we define the UI and server for our map component. The key
change is that the server function now accepts a registry
argument and calls register_leaflet()
itself.
# /path/to/your/app/map_module.R
#' Map Module UI
#'
#' @param id A character string. The namespace ID.
#' @return A UI definition.
mapUI <- function(id) {
ns <- NS(id)
leafletOutput(ns("wastewater_map"), height = "500px")
}
#' Map Module Server
#'
#' @param id A character string. The namespace ID.
#' @param data A reactive expression returning the data for the map.
#' @param registry A link_registry object for managing component linking.
mapServer <- function(id, data, registry) {
moduleServer(id, function(input, output, session) {
# Register this component with the central registry
register_leaflet(
session = session, # <-- Pass the module's session for namespacing
registry = registry,
leaflet_output_id = "wastewater_map", # <-- The local ID within this module
data_reactive = data,
shared_id_column = "id",
click_handler = function(map_proxy, selected_data, session) { # <-- click handler must have map_proxy, selected_data, session, overrides all default behavior
print("The leaflet map component was just clicked!")
}
)
output$wastewater_map <- renderLeaflet({
# ... leaflet rendering logic ...
})
})
}
Step 2: Create the Table Module (table_module.R
)
We do the same thing for our table module. It also accepts the
registry
and registers its own DT
output.
# /path/to/your/app/table_module.R
#' Table Module UI
#'
#' @param id A character string. The namespace ID.
#' @return A UI definition.
tableUI <- function(id) {
ns <- NS(id)
DTOutput(ns("wastewater_table"))
}
#' Table Module Server
#'
#' @param id A character string. The namespace ID.
#' @param data A reactive expression returning the data for the table.
#' @param registry A link_registry object for managing component linking.
tableServer <- function(id, data, registry) {
moduleServer(id, function(input, output, session) {
# Register this component with the central registry
register_dt(
session = session, # <-- Pass the module's session
registry = registry,
dt_output_id = "wastewater_table", # <-- The local ID
data_reactive = data,
shared_id_column = "id",
click_handler = function(map_proxy, selected_data, session) { # <-- click handler must have map_proxy, selected_data, session, overrides all default behavior
print("The DT table component was just clicked!")
}
)
output$wastewater_table <- renderDT({
# ... datatable rendering logic ...
})
})
}
Step 3: Assemble the Main App (app.R
)
Finally, the main app ties everything together. Notice how clean the
server logic is. It’s only responsible for creating the registry and
passing it to the modules. It has no knowledge of the internal component
IDs ("wastewater_map"
or
"wastewater_table"
).
# /path/to/your/app/app.R
library(shiny)
library(leaflet)
library(DT)
library(linkeR)
# Source the modules
source("map_module.R")
source("table_module.R")
# --- UI ---
ui <- fluidPage(
titlePanel("linkeR Modular Linking Example"),
fluidRow(
column(7,
h4("Wastewater Map (Module 1)"),
mapUI("map_module")
),
column(5,
h4("Facility Data (Module 2)"),
tableUI("table_module")
)
)
)
# --- Server ---
server <- function(input, output, session) {
# --- 1. Create the central link registry ---
registry <- create_link_registry(session)
# Shared reactive data
wastewater_data <- reactive({
# ... data generation logic ...
})
# --- 2. Pass the registry to each module server ---
mapServer("map_module", wastewater_data, registry)
tableServer("table_module", wastewater_data, registry)
}
shinyApp(ui, server)
This pattern ensures that your modules remain self-contained and
reusable while allowing linkeR
to correctly identify and
link components across your entire application.