11

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?

sdgfsdh
  • 33,689
  • 26
  • 132
  • 245
  • 3
    Could you use a "Go!" button so it only fires the calculation once the user has selected everything? – MarkeD Jul 01 '15 at 12:48
  • 2
    This is a duplicate of http://stackoverflow.com/questions/31051133/how-do-i-make-sure-that-a-shiny-reactive-plot-only-changes-once-all-other-reacti more or less which I answered. In brief, you combine reactive values with the timer. – Nick Kennedy Jul 01 '15 at 13:11
  • Using action button is definitely the way to go. If you choose scheduled events and user is "to slow" you're back to square one. – zero323 Jul 01 '15 at 13:23
  • 1
    Including a button will be quite clean wa of doing it as suggest by @MarkeD and zero323 and by . You can store the computations as an index to monitor which ones already have been computed and only compute the new when selectizeInput variables are not %in% the already computed ones – Pork Chop Jul 01 '15 at 13:25
  • @NickK Not quite - in your example you make a loop which polls the input. I am asking if it is possible to delay a reactive - it appears that you can't! – sdgfsdh Jul 01 '15 at 13:36
  • 1
    @sdgfsdh actually that's not what my code does. It doesn't poll the inputs any more than happens normally. When an input gets triggered, instead of the output being updated, a timer is initiated. If another input is triggered before the timer elapses, the timer is reset. Otherwise when the timer elapses, the output is updated. If you want to check this out, insert a few `cat` or `print` statements and watch the console. That seems to be what you were after here. If it's not, perhaps you could elaborate. In general, though, I think a button is probably the way to go. – Nick Kennedy Jul 01 '15 at 14:16

1 Answers1

13

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)
sdgfsdh
  • 33,689
  • 26
  • 132
  • 245