34  ๐Ÿ— R6

Published

2025-06-26

WARNING

This chapter is being developed. Thank you for your patience.

As your Shiny application grows in complexity, organizing code in a modular and object-oriented way becomes crucial. While Shiny is inherently reactive and functional, R6 classes provide a powerful tool for encapsulating state, behavior, and logic โ€” particularly useful in large-scale apps or when designing reusable components inside an R package.

In this chapter, weโ€™ll explore:


34.1 What is R6?

R6 is an object-oriented system in R that enables the creation of reference-based objects. Unlike S3 or S4 objects, R6 objects maintain state and support encapsulation, inheritance, and private/public members โ€” much like objects in other OOP languages (e.g., Python, Java).

This makes R6 especially useful for:

  • Managing mutable state (e.g., counters, user sessions)
  • Bundling business logic with data
  • Reusing logic across modules or apps

In an app packaged as an R package, R6 classes help you separate concerns and avoid global state.


34.1.1 Defining an R6 Class

You define R6 classes using the R6 package. Hereโ€™s a simple example of an R6 class for a counter:

# In R/counter.R
Counter <- R6::R6Class(
  "Counter",
  public = list(
    value = 0,
    initialize = function(start = 0) {
      self$value <- start
    },
    increment = function() {
      self$value <- self$value + 1
    },
    reset = function() {
      self$value <- 0
    },
    get = function() {
      self$value
    }
  )
)

Place this class definition inside your R/ directory so it is loaded with the rest of your package code.

34.1.2 Using R6 in the Shiny Server Function

To use the class in your app, instantiate it inside the server function and wrap its methods in reactive or observe blocks where needed:

# In inst/app/server.R

server <- function(input, output, session) {
  counter <- Counter$new()

  observeEvent(input$increment_btn, {
    counter$increment()
  })

  observeEvent(input$reset_btn, {
    counter$reset()
  })

  output$counter_text <- renderText({
    paste("Current count:", counter$get())
  })
}

And in the UI:

# In inst/app/ui.R

ui <- fluidPage(
  actionButton("increment_btn", "Increment"),
  actionButton("reset_btn", "Reset"),
  textOutput("counter_text")
)

34.2 Reactivity and R6

R6 objects arenโ€™t inherently reactive. If you want your app to respond to changes in an R6 objectโ€™s state, you need to wrap getters in reactive expressions or use reactiveVal/reactiveValues internally.

34.3 Strategy 1: Wrap method calls in render* functions

This is the simplest method, shown above with renderText().

34.4 Strategy 2: Use reactive triggers

You can also use reactiveVal to track state and trigger UI updates:

server <- function(input, output, session) {
  counter <- Counter$new()
  trigger <- reactiveVal(0)

  observeEvent(input$increment_btn, {
    counter$increment()
    trigger(trigger() + 1)
  })

  output$counter_text <- renderText({
    trigger()
    paste("Current count:", counter$get())
  })
}

34.5 Organizing R6 Classes in a Package

To integrate smoothly with your app-package:

  • Place class definitions in R/ with clear, modular file names (e.g., R/counter.R)
  • Use @export roxygen tags if you want to make classes available to users
  • Test classes with testthat as you would with functions
  • Avoid instantiating R6 objects at the package level (e.g., outside of functions); do so inside server() or module server functions

34.6 Example: Session-Aware Logger

Suppose we want an R6 class that logs user actions with session tracking:

# In R/logger.R
Logger <- R6::R6Class("Logger",
  public = list(
    session_id = NULL,
    logs = character(),
    
    initialize = function(session_id) {
      self$session_id <- session_id
    },
    
    log = function(message) {
      timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
      entry <- paste0("[", self$session_id, " @ ", timestamp, "] ", message)
      self$logs <- c(self$logs, entry)
    },
    
    get_logs = function() {
      self$logs
    }
  )
)

In the server:

server <- function(input, output, session) {
  logger <- Logger$new(session$token)

  observeEvent(input$do_something, {
    logger$log("Button clicked")
  })

  output$log_output <- renderPrint({
    logger$get_logs()
  })
}

34.7 Recap

Using R6 in Shiny app-packages gives you a scalable, maintainable approach to organizing business logic and managing state. It shines when:

  • You want object-oriented abstractions
  • You need mutable objects tied to user sessions
  • Youโ€™re building reusable modules within or across apps

While not every Shiny app needs R6, mastering this tool adds a robust pattern to your Shiny developer toolkit โ€” particularly when packaging your app for production or distribution.

In the next chapter, weโ€™ll explore how to use Shiny modules in combination with R6 objects to further encapsulate functionality within your package structure.