Skip to Content
Course content

44: Building Multi-Page Shiny Apps

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

When you first realize your Shiny app needs more than one screen, the instinct is usually to treat the UI like a slide deck. I've seen this a dozen times: a developer adds a selectInput or a few action buttons to act as a "menu," then wraps every single piece of the UI in conditionalPanel or a massive uiOutput. It feels intuitive because you're essentially just telling Shiny, "If the user clicked Page B, hide Page A and show this other stuff."

The "Toggling" Trap

The problem with this approach isn't that it doesn't work—it's that it scales terribly. Imagine you're building a Corporate Finance Dashboard. You have a "Revenue Overview" page and a "Detailed Expenses" page. Using conditionalPanel, your ui.R becomes a monolithic wall of code. You're scrolling through five hundred lines of nested DIVs just to find a single plot's output ID.

But the real nightmare happens in the server logic. Since everything is technically "active" even if it's hidden from the user, your server function becomes a giant tangle of observeEvent and reactive blocks. You'll find yourself writing guards like if (input$page == "Expenses") { ... } inside every single reactive expression just to prevent the app from trying to calculate expense totals while the user is still looking at the revenue page. It's fragile, it's hard to test, and one typo in a string like "Expenses" can silently break a whole section of your app.

# The naive way: Everything in one giant bucket
ui <- fluidPage(
  selectInput("page", "Navigate", choices = c("Overview", "Details")),
  conditionalPanel(
    condition = "input.page == 'Overview'",
    plotOutput("overview_plot")
  ),
  conditionalPanel(
    condition = "input.page == 'Details'",
    tableOutput("details_table")
  )
)

Decoupling with navbarPage and Modules

The professional way to handle this is to stop thinking about "toggling visibility" and start thinking about "isolated components." For most apps, navbarPage is the structural answer, but the secret sauce is combining it with Shiny Modules. Instead of one giant server function, you create a separate server function for each page.

By wrapping each page in a module, you create a namespace. This means your "Overview" page can have an output called plot and your "Details" page can also have an output called plot without them colliding. You aren't just organizing your code; you're creating a boundary. If the "Details" page crashes because of a data type error, it's much easier to isolate the bug when that page's logic is encapsulated in its own server_details function rather than buried in a 1,000-line file.

# The professional way: Modularized pages
# In a separate file: modules_overview.R
overviewUI <- function(id) {
  ns <- NS(id)
  tagList(plotOutput(ns("plot")))
}

overviewServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    output$plot <- renderPlot({ ... })
  })
}

# In app.R
ui <- navbarPage("Finance Dashboard",
  tabPanel("Overview", overviewUI("overview")),
  tabPanel("Details", detailsUI("details"))
)

server <- function(input, output, session) {
  overviewServer("overview")
  detailsServer("details")
}

The Cost of the Shift

I'll be honest: this adds a bit of boilerplate. You have to deal with NS(id) and the overhead of calling moduleServer. If you're building a tiny internal tool with two screens, this is probably overkill. But the moment your app is intended for production or will be maintained by a team, the trade-off is a no-brainer. You trade a few extra lines of setup for the ability to work on the "Details" page without worrying that you're accidentally breaking the "Overview" page. You're moving from a script that happens to be an app to a software architecture.




📋 Practical Task

Build a Modular Environmental Monitoring Dashboard

You are tasked with upgrading a messy, single-file environmental app into a professional multi-page structure. The app needs two distinct sections: Air Quality and Water Quality.

Your goal is to implement the following requirements:

  • Use navbarPage to create the main navigation.
  • Create two separate Shiny Modules (one for Air Quality, one for Water Quality).
  • Each module must contain its own UI function and Server function.
  • Inside the Air Quality module, include a plotOutput that displays a random line chart.
  • Inside the Water Quality module, include a tableOutput that displays a small data frame of "pH levels" and "Turbidity".
  • Ensure that both modules use their own namespaces (NS) so that the IDs do not conflict.

Constraint: Do not use conditionalPanel or uiOutput for the page switching; the navigation must be handled by the navbarPage architecture.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.