Search code examples
rtimerdelayshinyreactive-programming

In Shiny apps for R, how do I delay the firing of a reactive?


I have a selectizeInput in my Shiny app. It is in multiple-select mode, so the user can specify more than one selection.

However, the reactives that depend on the selectizeInput get fired every time a selection is added. Suppose that the user intends to select A, B and C. Currently, my app will do it expensive computations for the selections A, A, B and A, B, C, when only the last is required.

The best way I can think to solve this is to delay the firing of the selectizeInput by a second or so to give the user a chance to enter all of the selections. Each new selection should set the timer back to 1 second. I know that Shiny provides an invalidateLater command, but this causes the reactive to fire once now and once later.

How can I get the reactive to only fire once later?


Solution

  • You should debounce the reactive.

    There is an R implementation here: https://gist.github.com/jcheng5/6141ea7066e62cafb31c

    # Returns a reactive that debounces the given expression by the given time in
    # milliseconds.
    #
    # This is not a true debounce in that it will not prevent \code{expr} from being
    # called many times (in fact it may be called more times than usual), but
    # rather, the reactive invalidation signal that is produced by expr is debounced
    # instead. This means that this function should be used when \code{expr} is
    # cheap but the things it will trigger (outputs and reactives that use
    # \code{expr}) are expensive.
    debounce <- function(expr, millis, env = parent.frame(), quoted = FALSE,
      domain = getDefaultReactiveDomain()) {
      
      force(millis)
      
      f <- exprToFunction(expr, env, quoted)
      label <- sprintf("debounce(%s)", paste(deparse(body(f)), collapse = "\n"))
    
      v <- reactiveValues(
        trigger = NULL,
        when = NULL # the deadline for the timer to fire; NULL if not scheduled
      )  
    
      # Responsible for tracking when f() changes.
      observeEvent(f(), {
        # The value changed. Start or reset the timer.
        v$when <- Sys.time() + millis/1000
      }, ignoreNULL = FALSE)
    
      # This observer is the timer. It rests until v$when elapses, then touches
      # v$trigger.
      observe({
        if (is.null(v$when))
          return()
        
        now <- Sys.time()
        if (now >= v$when) {
          v$trigger <- runif(1)
          v$when <- NULL
        } else {
          invalidateLater((v$when - now) * 1000, domain)
        }
      })
    
      # This is the actual reactive that is returned to the user. It returns the
      # value of f(), but only invalidates/updates when v$trigger is touched.
      eventReactive(v$trigger, {
        f()
      }, ignoreNULL = FALSE)
    }
    
    
    #' @examples
    #' library(shiny)
    #' 
    #' ui <- fluidPage(
    #'   numericInput("val", "Change this rapidly, then pause", 5),
    #'   textOutput("out")
    #' )
    #' 
    #' server <- function(input, output, session) {
    #'   debounced <- debounce(input$val, 1000)
    #'   output$out <- renderText(
    #'     debounced()
    #'   )
    #' }
    #' 
    #' shinyApp(ui, server)