S3 methods, custom getters and setters for reactivevalues

Hi Shiny community,

I work with shiny to build a large-ish app (currently about 60 different modules, many of which get called multiple times).
I use one central reactivevalues object, called values, to let Shiny modules interact with each other. values contains a set of reactivevalues objects. This is seems quite convenient because modifications in that central values object can be observed in all modules, and rearranging modules is much easier when objects that need to be accessed by multiple modules don't have to be passed in through callModule() individually, but instead are always found in the same place, no matter where the modules are called. Thanks to reference semantics, it is my understanding that all modules also access and change the same object in memory.

However, accessing the correct objects inside values gets a bit verbose and if the structure of values changes, I have to review the code of all modules.

In regular R programming, the natural reflex would be to formalize the values object into a S3 or S4 (or, for that matter, an R6) class and write methods that know how to safely retrieve or modify data.

Is there a best practice for this situation in shiny?

I started assigning a custom class name to the values object and S3 generics type getters and setters to make things easier to navigate. This seems to work, but I want to make sure that this does not end up being a terrible idea.

  1. Objects created with reactiveValues are of class reactivevalues, and changing that to c("myClass", "reactivevalues") (as seen in the example below) seems to work. Would you expect a problem with this?

  2. The example below is a bit complicated, but also demonstrates a number of things:

  • class of an reactivevalues is changed (in a way that would still return is.reactivevalues() = TRUE)
  • values retrieved with a getter are observable
  • getter (or setter) can include an update (or any other kind of check on the returned value) that may be useful
  • in this example, the button using the getter has a (somewhat artificially introduced) advantage and always adds the number currently specified in the numericInput
  1. Would it be "best practice" to extend the underlying R6 class (shiny:::ReactiveValues) in a subclass that has additional methods? This seems like it would be much more complicated and have more potential for breaking things, but I''m curious if that is an option. Given that shiny:::ReactiveValues is not portable, in my very limited understanding of R6 classes, cross-package compatibility would be a potential problem.

I hope this is not too convoluted, I'll be happy to clarify and would love to get some feedback, thank you!

Max

library(shiny)


#Defining some getter and setter functions

#note that here, method dispatch depends on the value being set,
#which is a bit sneaky, but might be useful when different processing
#is required for different things being set
'Setter<-' <- function(x, value, ...){
    
    UseMethod('Setter<-', value)
    
}

'Setter<-.numeric' <- function(x, value){
        x$exampleRV$var1 <- value
}

'Getter' <- function(x, ...){
   
    UseMethod('Getter', x)
    
}

'Getter.myClass' <- function(x){
        return(x$exampleRV)
    }

'GetInput' <- function(x, ...){
   
    UseMethod('GetInput', x)
    
}

# this getter is special because it makes sure that the 
# value it is getting gets updated from elsewhere when needed
# isolate could also be made optional
'GetInput.myClass' <- function(x, update = T){
    if(update){
    isolate({
   x$exampleRV$addthis <- x$input$addthis
    })
    }
        return(x$exampleRV$addthis)
}

 # using an observer like this would be of course be preferable in many cases 
#  observeEvent(input$addthis,{values$exampleRV$addthis <- input$addthis})


server <- shinyServer(function(input, output, session) {
  
    
    values <- reactiveValues(exampleRV = reactiveValues(var1 = 1,
                                                                var2 = 1,
                                                                addthis = 1))
    
    #assigning and additional class to this reactivevalues object
    class(values) <- c("myClass", class(values))
  
    #making input accessible from values
    observeEvent(values,{values$input <- input}, once = T)
    
###Observers for the actionButtons
    
    #Getter and Setter function work
  observeEvent(input$ab1,{
        Setter(values) <- Getter(values)$var1 + values$exampleRV$addthis
  })
  
  #In this variant, the addthis value is retrieved by a custom getter
  observeEvent(input$ab2,{
        Setter(values) <- Getter(values)$var1 + GetInput(values)
  })
   
### Values returned by getters are observable as if using their return value directly
  observeEvent(Getter(values)$var1,{
        print(paste0("exampleList$var1 (",Getter(values)$var1,") triggered"))
      })
  
 # as expected, this does not get triggered by chages to var1
  observeEvent(Getter(values)$var2,{
        print(paste0("exampleList$var2 (",Getter(values)$var2,") triggered"))
      })
  

  output$diag <- renderPrint({
     print(reactiveValuesToList(Getter(values)))
  })
  
})

ui <- fluidPage(
    actionButton("ab1","Add number"),
    actionButton("ab2","Add updated number using Getter"),
    numericInput("addthis","Add this number", value = 1),
    p("This is a printout of the 'values$exampleRV' reactivevalues object:"),
    verbatimTextOutput('diag')
    )
shinyApp(ui,server)

This topic was automatically closed 54 days after the last reply. New replies are no longer allowed.