Good way to create a "reactive-aware" R6 class

Suppose I have a simple Person class:

Person <- R6::R6Class(
  "Person",
  private = list(name = ""),
  public = list(
    initialize = function(name) {
      private$name <- name
    },
    print = function() {
      cat("Person:", private$name)
    },
    changeName = function(newName) {
      private$name <- newName
    }
  )
)

Now suppose I want to have a Person object in a shiny app as a reactiveVal(). I would instantiate it with person <- reactiveVal(Person$new("Dean")).

If I create an observer that executes when the person changes, I would use observeEvent(person(), ...). Now if I change the person using person(Person$new("Ben")) then it would trigger.

However, if I modify the underlying Person object, for example with person$changeName("Ben"), then that won't invalidate the reactive.

My question is: how would you best implement this in a way that any changes to the R6 object would trigger the reactive? I can think of lots of hacky solutions, but I'm trying to come up with an elengant generic solution. I think the ideal solution would involve only code changes in the Person class, so that ideally you could say "if name or age change, that should invalidate the Person, but if weight changes then it doesn't trigger invalidation".

I'd love to see if others have come across this and if anyone has a very clean solution.

Here's one possibility:

  • Add an internal reactiveVal, and when any methods are called that change the object, change the reactiveVal so it invalidates reactive dependencies.
  • Add a getReactive() method which calls the reactiveVal so that a reactive dependency is formed, and then returns self.
  • Wrap the call to $getReactive() in a reactive expression.

For example:

library(shiny)
Person <- R6::R6Class(
  "Person",
  private = list(
    name = "",
    reactiveDep = NULL
  ),
  public = list(
    initialize = function(name) {
      private$reactiveDep <- reactiveVal(0)
      private$name <- name
    },
    print = function() {
      cat("Person:", private$name)
    },
    changeName = function(newName) {
      private$reactiveDep(isolate(private$reactiveDep()) + 1)
      private$name <- newName
    },
    getName = function() {
      private$name
    },
    getReactive = function() {
      private$reactiveDep()
      self
    }
  )
)

# Create the Person object and wrap it in a reactive expression
p <- Person$new("Dean")
pr <- reactive(p$getReactive())

# The observer accesses the reactive expression
o <- observe({
  message("Person changed. Name: ", pr()$getName())  
})
shiny:::flushReact()
#> Person changed. Name: Dean

p$changeName("Newname")
shiny:::flushReact()
#> Person changed. Name: Newname

# If changeName isn't called, observeEvent isn't triggered:
shiny:::flushReact()

# Clean up observer so it doesn't stick around in the session
o$destroy()
#> NULL

Any methods that alter the object will have to also have to change private$reactiveDep.

Thanks @winston that seems like a promising idea. It looks like the code doesn't fully match the explanation, specifically the second bullet point

Add a getReactive() method which changes the reactiveVal and returns self

Doing something like that was what I wanted to achieve, but that's not what the code is doing and I don't know how to get that to work!

Oops, sorry about that -- my description was slightly wrong, and the code didn't quite do what I had said. I've edited both. Hope that helps!

It looks like simply returning self from getReactive() does indeed result in your code working in a shiny app! That solution is very elegant and works well. The one thing that bothers me about it is that it requires two variables, and there's a bit of a disconnect between the two - I have to use one variable for calling methods and one variable for following reactivity, which results in more bookkeeping.

The best solution I could come up with is not as elegant as yours, but I focused on trying to get away with having only one variable, not two. It doesn't work with reactive, only with reactiveVal, and it has a very ugly part that requires you to initialize the class and then pass it a pointer of itself. It also uses internal features of shiny that have no guarantee to stay the same in future versions. Here is what I came up with:

Person <- R6::R6Class(
  "Person",
  private = list(
    name = "",
    rv = NULL
  ),
  public = list(
    initialize = function(name) {
      private$name <- name
    },
    print = function() {
      cat("Person:", private$name)
    },
    changeName = function(newName) {
      if (!is.null(private$rv)) {
        get("rv", environment(private$rv))$private$dependents$invalidate()
      }
      private$name <- newName
    },
    add_shiny_rv = function(rv) {
      private$rv <- rv
    }
  )
)

And to use it, I would need to do

pr <- reactiveVal(Person$new("Dean"))
isolate(pr()$add_shiny_rv(pr))

The good thing is that now pr() is used both for calling methods on the object, and for reactivity. But it's so hacky that I'm sure it's dangerous to use such code.

For completeness, here's the solution by @winston inclduing the tiny fix I mention at the top of this post:

library(shiny)

Person <- R6::R6Class(
  "Person",
  private = list(
    name = "",
    reactiveDep = NULL
  ),
  public = list(
    initialize = function(name) {
      private$reactiveDep <- reactiveVal(0)
      private$name <- name
    },
    print = function() {
      cat("Person:", private$name, "\n")
    },
    changeName = function(newName) {
      private$reactiveDep(isolate(private$reactiveDep()) + 1)
      private$name <- newName
    },
    getReactive = function() {
      private$reactiveDep()
      self
    }
  )
)

p <- Person$new("Dean")
pr <- reactive(p$getReactive())

I have an improved version of my previous code: instead creating two variables, add a method called $reactive() that can be chained off of the call to $new().

library(shiny)
Person <- R6::R6Class(
  "Person",
  private = list(
    name = "",
    reactiveDep = NULL
  ),
  public = list(
    initialize = function(name) {
      private$reactiveDep <- reactiveVal(0)
      private$name <- name
    },
    reactive = function() {
      reactive({
        private$reactiveDep()
        self
      })
    },
    print = function() {
      cat("Person:", private$name)
    },
    changeName = function(newName) {
      private$reactiveDep(isolate(private$reactiveDep()) + 1)
      private$name <- newName
    },
    getName = function() {
      private$name
    }
  )
)

p <- Person$new("Dean")$reactive()

# The observer accesses the reactive expression
o <- observe({
  message("Person changed. Name: ", p()$getName())  
})
shiny:::flushReact()
#> Person changed. Name: Dean

# Note that this is in isolate only because we're running at the console; in 
# typical Shiny code, the isolate() wouldn't be necessary.
isolate(p()$changeName("Newname"))
shiny:::flushReact()
#> Person changed. Name: Newname

# If changeName isn't called, observeEvent isn't triggered
shiny:::flushReact()

# Clean up observer so it doesn't stick around in the session
o$destroy()

Note that you if you don't want any reactive behavior, you can just call p <- Person$new("Dean"). Later calls to p$changeName("Foo") will be fine, even outside of Shiny, because setting a reactiveVal can be done anywhere -- only reading it requires a reactive context.

That's prefect, this is exactly what I was looking for. Much better than my "solution"! Thanks @winston

1 Like

One more version, with a few improvements:

  • The $reactive() method only creates a reactive expression object once; then it stores the reactive expression so if $reactive() is called again, it returns the same object. (Note that this is good for most use cases, but not all.)
  • The reactiveVal and reactive are created only if needed (if someone calls the $reactive() method).
  • Removed internal use of isolate(). This reduces overhead, and makes it easier to use the class without Shiny if desired.
Person <- R6::R6Class(
  "Person",
  private = list(
    name = "",
    reactiveDep = NULL,
    reactiveExpr = NULL,
    invalidate = function() {
      private$count <- private$count + 1
      private$reactiveDep(private$count)
      invisible()
    },
    count = 0
  ),
  public = list(
    initialize = function(name) {
      # Until someone calls $reactive(), private$reactiveDep() is a no-op. Need
      # to set it here because if it's set in the definition of private above, it will
      # be locked and can't be changed.
      private$reactiveDep <- function(x) NULL
      private$name <- name
    },
    reactive = function() {
      # Ensure the reactive stuff is initialized.
      if (is.null(private$reactiveExpr)) {
        private$reactiveDep <- reactiveVal(0)
        private$reactiveExpr <- reactive({
          private$reactiveDep()
          self
        })
      }
      private$reactiveExpr
    },
    print = function() {
      cat("Person:", private$name)
    },
    changeName = function(newName) {
      private$name <- newName
      private$invalidate()
    },
    getName = function() {
      private$name
    }
  )
)

pr <- Person$new("Dean")$reactive()

# The observer accesses the reactive expression
o <- observe({
  message("Person changed. Name: ", pr()$getName())  
})
shiny:::flushReact()
#> Person changed. Name: Dean

# Note that this is in isolate only because we're running at the console; in 
# typical Shiny code, the isolate() wouldn't be necessary.
isolate(pr()$changeName("Newname"))
shiny:::flushReact()
#> Person changed. Name: Newname
1 Like

I also ended up adding a boolean parameter to initialize() that determines if to create the reactive variables or not, instead of creating it inside the reactive() method.

I don't actually have a usecase for this yet, this was entirely a theoretical question because I find myself increasingly writing R6 classes and wanted to know that if I ever nees to combine them into shiny natively it would work. Hopefully others will find this useful. Thanks a lot Winston

1 Like

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

If you have a query related to it or one of the replies, start a new topic and refer back with a link.

The solution above from Winston is great, and it essentially gives you a reactive value that invalidates every time the R6 object has changed.

I just came across a similar need where instead of having a reactive value mirroring the state of the R6 object, I wanted to have a reference to the non-reactive R6 object but be able to use reactive methods on it. This is slightly different, and here is a nice way to achieve that: (it uses the reactive trigger construct that Joe Cheng came up with a long time ago)

reactive_trigger <- function() {
  rv <- shiny::reactiveVal(0)
  list(
    depend = function() {
      invisible(rv())
    },
    trigger = function() {
      rv(isolate(rv() + 1))
    }
  )
}

Person <- R6::R6Class(
  "Person",
  private = list(
    name = "",
    rx_trigger = NULL,
    depend = function() {
      if (!is.null(private$rx_trigger)) private$rx_trigger$depend()
    },
    trigger = function() {
      if (!is.null(private$rx_trigger)) private$rx_trigger$trigger()
    }
  ),
  public = list(
    initialize = function(name, reactive = FALSE) {
      private$name <- name
      if (reactive) {
        private$rx_trigger <- reactive_trigger()
      }
    },
    print = function() {
      private$depend()
      cat("Person:", private$name)
    },
    changeName = function(newName) {
      private$name <- newName
      private$trigger()
    },
    getName = function() {
      private$depend()
      private$name
    }
  )
)

pr <- Person$new("Dean", reactive = TRUE)

# The observer accesses the reactive expression
o <- observe({
  message("Person changed. Name: ", pr$getName())  
})
shiny:::flushReact()
#> Person changed. Name: Dean

# Note that this is in isolate only because we're running at the console; in 
# typical Shiny code, the isolate() wouldn't be necessary.
isolate(pr$changeName("Newname"))
shiny:::flushReact()
1 Like