Managing state and business logic with R6 classes in Shiny app-packages
R6
Shiny
Author
Martin Frigaard
Published
July 5, 2026
As your Shiny application grows, organizing code into modular, object-oriented pieces becomes crucial. While Shiny’s reactive programming model is naturally functional, R6 classes provide a powerful abstraction for encapsulating state, behavior, and business logic. If you’re building a Shiny app as an R package, R6 brings real benefits: clean separation of concerns, easier testing, and reusable module patterns that survive across projects.
In this post, we’ll explore:
What R6 is and why it shines in Shiny app-packages
Defining R6 classes for Shiny modules (UI and server)
Wiring R6 objects into your app’s reactive flow
A real working example from a stock volatility dashboard
What is R6?
R6 is an object-oriented system in R that creates reference-based objects. Unlike S3 or S4, R6 objects are mutable, support encapsulation (public and private members), and feel familiar to developers from Python or Java.
R6 is especially useful for:
Bundling UI generation with business logic (module as a unified object)
Managing state tied to a user session or module instance
Encapsulating dependencies and configuration
In an app-package, R6 prevents you from scattering related logic across separate files and avoids the need for global state or overly nested reactive functions.
Defining an R6 Shiny Module
An R6 module wraps both its UI and server logic into one class. Here’s a simplified example from the Rsixer stock dashboard app:
Each module is instantiated fresh per session, so there’s no global state. Reactive dependencies flow naturally between them.
Reactivity and R6
R6 objects themselves are not reactive; they’re just containers for logic. To make your app respond to changes in an R6 module’s state, you must return reactive expressions from the server() method.
In ModInputs, the server() method returns a reactive list:
server =function() { shiny::moduleServer(private$id, function(input, output, session) {# Return a reactive list so downstream modules can access tickers, dates, etc. shiny::reactive({list(tickers = input$tickers,from = input$dates[1],to = input$dates[2] ) }) })}
Then in ModOutputs, you consume that reactive list:
server =function(inputs_r) { shiny::moduleServer(private$id, function(input, output, session) {# Downstream modules receive inputs_r and call it as a function prices_r <- shiny::eventReactive(inputs_r()$fetch, { inp <-inputs_r()get_stock_prices(tickers = inp$tickers,from = inp$from,to = inp$to ) }) })}
This pattern keeps the dependency graph explicit and testable.
Organizing R6 Classes in a Package
Structure your R6 classes for clarity and maintainability:
Place each class definition in R/mod_*.R (one file per module)
Use @export roxygen tags to make classes available to package users
Write tests with testthat that instantiate and call the class methods
Never instantiate R6 objects at package load time; only inside app_server() or module server functions
Keep private members truly private; expose only what downstream modules need
Example from Rsixer’s package structure:
R/
mod_inputs.R # ModInputs class
mod_outputs.R # ModOutputs class
mod_download.R # ModDownload class
app_server.R # Wires them together
app_ui.R # Calls ui() methods
Recap
R6 classes provide a clean, testable way to organize Shiny app-packages. By bundling UI generation and server logic into a single object, you gain encapsulation, reduce global state, and make dependencies explicit. It’s a pattern that scales; the Rsixer app uses R6 modules for inputs, outputs, and downloads, each passing reactive values to the next in a clear data flow.
While not every Shiny app needs R6, it’s worth learning if you’re building production packages or multi-developer projects. It turns your modules into reusable components that you (or your team) can confidently import into future apps.