R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
137: Building Multi-Page Shiny Dashboards with bslib
We’ve all been there: you build a Shiny app that starts as a simple one-page prototype, and it's a hit. Suddenly, your stakeholders want "just a few more things." You add a page for raw data, a page for a detailed map, and a page for settings. Before you know it, your ui.R is a wall of nested tabPanel calls and your server.R is a 1,500-line monolith where you're terrified to rename a single outputId because you might break a reactive chain three hundred lines away.
The Single-File Monolith
The naive way to build a multi-page app is to treat the entire dashboard as one giant object. You likely use navbarPage() or tabsetPanel() and just keep appending tabPanel() arguments. In the server, you write every output for every page in one long sequence. It looks something like this:
ui <- navbarPage(
"Company Health",
tabPanel("Revenue", plotOutput("rev_plot"), tableOutput("rev_table")),
tabPanel("Churn", plotOutput("churn_plot"), tableOutput("churn_table")),
tabPanel("Feedback", plotOutput("sent_plot"))
)
server <- function(input, output, session) {
# Revenue logic
output$rev_plot <- renderPlot({ ... })
output$rev_table <- renderTable({ ... })
# Churn logic
output$churn_plot <- renderPlot({ ... })
output$churn_table <- renderTable({ ... })
# Feedback logic
output$sent_plot <- renderPlot({ ... })
}
This works fine for a "Hello World" app, but it scales terribly. The biggest issue isn't the UI—it's the namespace. In the example above, every outputId must be unique across the entire application. If you decide to add a second "Table" to the Revenue page, you're inventing names like rev_table_2. Worse, your server function becomes a dumping ground for reactive values that are only relevant to one specific page but are globally accessible to the whole session. It's a recipe for "spaghetti code" where a change to a filter on page one accidentally triggers a heavy calculation on page three.
Scaling with page_navbar and Modules
If you want to build something that won't make you cringe six months from now, you should pair bslib::page_navbar() with Shiny modules. bslib gives us a modern Bootstrap 5 foundation that's far more flexible than the legacy navbarPage, but the real magic happens when you stop putting your page logic in the main server function.
Instead of cramming everything into one file, I treat each page as its own mini-app. I create a UI function and a Server function for each page. This isolates the namespaces. When I'm working on the "Churn" page, I don't have to care about what's happening on the "Revenue" page.
# The Churn Module
churnUI <- function(id) {
ns <- NS(id)
tagList(
plotOutput(ns("churn_plot")),
tableOutput(ns("churn_table"))
)
}
churnServer <- function(id) {
moduleServer(id, function(input, output, session) {
output$churn_plot <- renderPlot({ ... })
output$churn_table <- renderTable({ ... })
})
}
# The Main App
ui <- page_navbar(
title = "Company Health",
nav_panel("Revenue", revUI("rev_page")),
nav_panel("Churn", churnUI("churn_page")),
nav_panel("Feedback", feedUI("feed_page"))
)
server <- function(input, output, session) {
revServer("rev_page")
churnServer("churn_page")
feedServer("feed_page")
}
The Boilerplate Tax vs. The Maintenance Debt
Now, I'll be honest: this approach feels like more work upfront. You have to wrap your IDs in ns(), and you have to write separate functions for every page. You're paying a "boilerplate tax."
But you're doing it to avoid "maintenance debt." When you use page_navbar and modules, you can move a page's logic into a separate R file entirely. If a bug appears in the Churn calculations, you know exactly which file to open. You can also reuse modules; if you suddenly need two "Revenue" views for different regions, you just call revUI("region_a") and revUI("region_b"). The modular approach transforms your app from a fragile house of cards into a set of LEGO bricks.
One final tip: use bslib's nav_spacer() within your page_navbar. It allows you to push certain links (like "Settings" or "Help") to the right side of the navigation bar, giving your app a professional, polished feel that the old navbarPage simply couldn't achieve without custom CSS hacks.
📋 Practical Task
Build a Regional Sales Performance Tracker
Your goal is to move away from a monolithic Shiny structure and build a modularized multi-page dashboard using bslib.
Requirements:
- Use
bslib::page_navbar()as the main layout. - Create two distinct modules:
regional_sales_ui/serverandproduct_breakdown_ui/server. - The Regional Sales page should contain a
plotOutputshowing a mock trend line and aselectInputto filter by region. - The Product Breakdown page should contain a
tableOutputshowing mock product performance and asliderInputto filter by minimum revenue. - Ensure that all
outputIdandinputIdreferences within the modules are correctly wrapped inns(). - In the main
page_navbar, include a thirdnav_panelfor "About" (this can be simple text) and usenav_spacer()to separate the main data panels from the "About" panel.
Success Criteria: The app should launch without namespace errors, and changing the filter on the Regional Sales page should not trigger a re-render of the Product Breakdown table.
There are no comments for now.