rstudio / shiny

Easy interactive web applications with R
https://shiny.posit.co/
Other
5.37k stars 1.86k forks source link

ignoreInit not working with dynamic content #2292

Open Kurt17P opened 5 years ago

Kurt17P commented 5 years ago

Hi, I recognized that observeEvent is triggered upon start when the UI is created dynamically, even with ignoreInit=TRUE. I found this thread on stackoverflow, but the presented solution does not work for checkboxInput because the value of the eventExpr is either T/F, so it's not possible to decide if it's the first call during initialization or a regular call.

In the minimal working example below I found a workaround by using shinyjs::delay, but I am not sure if this would always work and what minimum delay time is required.

To me, ignoreInit should also work for dynamic UIs, i.e. the observer for dynamicBox should not be triggered upon initialization as is the case for staticBox. Is there any chance to fix that issue or is this expected behavior?

library(shinydashboard)
library(shiny)
library(shinyjs)

ui <- dashboardPage(
  dashboardHeader(title = "observeEvent ignoreInit"),
  dashboardSidebar(),
  dashboardBody(
    useShinyjs(),
    checkboxInput("staticBox", "static"),
    uiOutput("body"))
)

server <- function(input, output, session) {

  output$body <- renderUI( tagList(
    checkboxInput("dynamicBox", "dynamic"),
    checkboxInput("dynamicBoxDelayed", "dynamic delayed")
  ))

  # observe static checkBox
  observeEvent(input$staticBox, {
    cat("observeEvent of static checkBox is executed\n")
  }, ignoreInit=TRUE)

  # observe dynamic checkBox
  observeEvent(input$dynamicBox, {
    # this is executed upon start, although ignoreInit is set to TRUE
    cat("observeEvent of dynamic checkBox is executed\n")
    cat( paste0("dynamicBox value = '", input$dynamicBox, "'\n") )
  }, ignoreInit=TRUE)

  # observe dynamicDelayed checkBox
  shinyjs::delay(100, {
    observeEvent(input$dynamicBoxDelayed, {
      cat("observeEvent of dynamic checkBoxDelayed is executed\n")
    }, ignoreInit=TRUE)
  })

}

shinyApp(ui, server)
m-saenger commented 4 years ago

I usually create a list (reactive) with default values for the UI items. In the event observers I check whether the values have changed (they shouldn't upon initialisation). If changed, consecutive actions are triggered. There might be a more elegant solution to this, however it works fine for me.

library(shiny)
library(rlang)

ui <- fluidPage(
  checkboxInput("staticBox", "static"),
  uiOutput("body"),
  textOutput("text")
)
server <- function(input, output, session) {

  # Buttons initial state
  buttons <- list(dynamicBox = FALSE, staticBox = FALSE, slider = 5)

  # Default Values (reactive not required in this minimal example)
  rv <- exec(reactiveValues, !!!buttons)

  # Render UI
  output$body <- renderUI(
    tagList(
      checkboxInput("dynamicBox", "dynamic"),
      sliderInput("slider", value = 5, min = 0, max = 10, step = 1, label = "Slider")
    ))

  # Build event observers in a loop
  sapply(names(buttons), function(i){
    observeEvent(input[[i]], { 
      if(rv[[i]] != input[[i]]){
        print(sprintf("observeEvent of %s is executed", i))
        output$text <- renderText({sprintf("observeEvent of %s is executed", i)})
      }
      rv[[i]] <- input[[i]]
    })
  })

}
shinyApp(ui, server)

# Example

# observeEvent(input$dynamicBox, { 
#   if(rv$dynamicBox != input$dynamicBox)
#     cat("observeEvent of dynamic checkBox is executed\n")
#   rv$dynamicBox <- input$dynamicBox
# })
# observeEvent(input$staticBox, { 
#   if(rv$staticBox != input$staticBox)
#     cat("observeEvent of static checkBox is executed\n")
#   rv$staticBox <- input$staticBox
# })
SarenT commented 2 years ago

I am affected by this issue as well and keeping track of the initial trigger worked for me. I wonder, if this can be easily fixed on the Shiny side.

jcheng5 commented 2 years ago

Tricky issue! ignoreInit is meant to ignore the initial call of the observer itself, not of the UI it’s referring to. But it is unintuitive indeed that it would work one way with static inputs and another with dynamic.

I do think this issue deserves a formal solution of some kind, but in the meantime a more convenient wrapper for @m-saenger’s “only invalidate when changed” approach is documented here: https://github.com/daattali/advanced-shiny/blob/master/reactive-dedupe/README.md

I’m not 100% sure this solves it without modification but hopefully provides a starting point.

bartekch commented 1 year ago

For me a convenient solution to this problem is to wrap eventExpr inside req. When silent req exception is raised inside eventExpr, observer is not executed, but what is more important it does not count as a call to the observer, specifically at the beginning it does not count as initialization. As a result, the observer is called for the first time when req gets truthy value for the first time, and if ignoreInit = TRUE, the observer is not executed during this first call. I believe that this is what OP was looking for. And it does work as well if input is generated directly, not in dynamic UI. Tested with shiny 1.7.4.

To be honest I'm not sure whether this is a desired behaviour, at least it is not documented (I didn't find anything), I figured it out partially by accident. Maybe @jcheng5 could shed more light on this.

Actual expression inside req depens on input type, for most of them it could be straightforward req(input), for checkBoxInput however it has to be req(!is.null(input)). Complete example from OP below.

library(shinydashboard)
library(shiny)
library(shinyjs)

ui <- dashboardPage(
  dashboardHeader(title = "observeEvent ignoreInit"),
  dashboardSidebar(),
  dashboardBody(
    useShinyjs(),
    checkboxInput("staticBox", "static"),
    uiOutput("body"))
)

server <- function(input, output, session) {

  output$body <- renderUI( tagList(
    checkboxInput("dynamicBox", "dynamic"),
    checkboxInput("dynamicBoxDelayed", "dynamic delayed")
  ))

  # observe static checkBox
  observeEvent(input$staticBox, {
    cat("observeEvent of static checkBox is executed\n")
  }, ignoreInit=TRUE)

  # observe dynamic checkBox
  observeEvent(req(!is.null(input$dynamicBox)), {
    # this is executed upon start, although ignoreInit is set to TRUE
    cat("observeEvent of dynamic checkBox is executed\n")
    cat( paste0("dynamicBox value = '", input$dynamicBox, "'\n") )
  }, ignoreInit=TRUE)

  # observe dynamicDelayed checkBox
  shinyjs::delay(100, {
    observeEvent(input$dynamicBoxDelayed, {
      cat("observeEvent of dynamic checkBoxDelayed is executed\n")
    }, ignoreInit=TRUE)
  })

}

shinyApp(ui, server)