R Shiny - The behaviour of sendInputMessage() vs sendCustomMessage() inside a for-loop

This is a cross-post from SE.

The app below contains an actionButton , a shinyWidgets::progressBar and a selectInput:

image

When the Start button is clicked, a for-loop is triggered which loops through the numbers 1-10 and increments the progress bar at each iteration. I would also like to update the value of the selectInput at each iteration but updateSelectInput does not work as expected. Instead of updating in tandem with the progress bar, the selectInput value is only updated once the loop terminates. I don't understand why updateProgressBar works here but updateSelectInput doesn't?

A commenter on SE suggested that it could be due to updateProgressBar using session$sendCustomMessage internally whereas updateSelectInput uses session$sendInputMessage. The only difference I can think of between the two is their arguments:

sendCustomMessage: function (type, message) 
sendInputMessage: function (inputId, message)

As per the help page for selectInputMessage:

sendInputMessage sends a message to an input on the session's client web page; if the input is present and bound on the page at the time the message is received, then the input binding object's receiveMessage(el, message) method will be called.

Here is the reproducible code:

library(shiny)
library(shinyWidgets)

ui <- fluidPage(
  actionButton(inputId = "go", label = "Start"), #, onclick = "$('#my-modal').modal().focus();"
  shinyWidgets::progressBar(id = "pb", value = 0, display_pct = TRUE),
  selectInput('letters', 'choose', letters)
)

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

  observeEvent(input$go, {

    shinyWidgets::updateProgressBar(session = session, id = "pb", value = 0) # reinitialize to 0 if you run the calculation several times

    for (i in 1:10) {

      updateProgressBar(session = session, id = "pb", value = 100/10*i)

      updateSelectInput(session, 'letters', selected = letters[i])

      Sys.sleep(.5)

    }

  })

}

shinyApp(ui = ui, server = server)

The help page mentions that the receiveMessage(el, message) method of the binding object is called when the sendInputMessage is received and I thought Sys.sleep might be interfering with this somehow but I can't be sure. My actual app uses removeUI and insertUI to change some text under the progress bar at each iteration of the for-loop. These use session$sendInsertUI and session$onFlushed internally but the behaviour is the same as the updateSelectInput. Below is an example that uses insertUI and removeUI:

library(shiny)
library(shinyWidgets)

ui <- fluidPage(
  actionButton(inputId = "go", label = "Start"), #, onclick = "$('#my-modal').modal().focus();"
  shinyWidgets::progressBar(id = "pb", value = 0, display_pct = TRUE),
  div(id = 'placeholder')
)

server <- function(input, output, session) {
  
  observeEvent(input$go, {
    
    shinyWidgets::updateProgressBar(session = session, id = "pb", value = 0) # reinitialize to 0 if you run the calculation several times
    
    for (i in 1:10) {
      
      updateProgressBar(session = session, id = "pb", value = 100/10*i)
      
      removeUI('#text')
      
      insertUI('#placeholder', ui = tags$p(id = 'text', paste('iteration:', i)))
      
      Sys.sleep(.5)
      
    }
    
  })
  
}

shinyApp(ui = ui, server = server)

I'm not sure what's going on here so any insight would be greatly appreciated.

EDIT: I know that all iterations in a for-loop run in the same scope whereas every iteration of an lapply has its own environment. But replacing the for-loop with lapply in my example does not work either.

It works if I set immediate = T in removeUI and insertUI . I got the idea from this post on SE - it doesn't explain why immediate = T is needed though. According to the help page:

Immediate - whether the UI object should be immediately inserted into the app when you call insertUI, or whether Shiny should wait until all outputs have been updated and all observers have been run (default).

But I don't understand what this means in the context of the for-loop in my example. Does it have something to do with the scope of the for-loop? I would really appreciate it if someone could shed some light on this.

Updated code:

library(shiny)
library(shinyWidgets)

ui <- fluidPage(
  actionButton(inputId = "go", label = "Start"), #, onclick = "$('#my-modal').modal().focus();"
  shinyWidgets::progressBar(id = "pb", value = 0, display_pct = TRUE),
  div(id = 'placeholder')
)

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

  observeEvent(input$go, {

    shinyWidgets::updateProgressBar(session = session, id = "pb", value = 0) # reinitialize to 0 if you run the calculation several times

    for (i in 1:10) {

      updateProgressBar(session = session, id = "pb", value = 100/10*i)

      removeUI('#text', immediate = T)

      insertUI('#placeholder', ui = tags$p(id = 'text', paste('iteration:', i)), immediate = T)

      Sys.sleep(1)

    }

  })

}

shinyApp(ui = ui, server = server)

Hi @m.merchant. I also don't exactly know why. But from the source code of shiny, I guess sendCustomMessage is direct send message and communicate with javascript. And sendInputMessage is depended on the shiny reactive behaviour. The reactive process one by one, so it is not the problem of for loop. The reactive fire after the for loop finish, so the selectInput update once. But progress bar fire message to javascript ten time in the for loop. You can test the behaviour with the follow code. The for loop should trigger the reactiveVal count ten time but the progress bar and selectInput only update one.

library(shiny)
library(shinyWidgets)

ui <- fluidPage(
  actionButton(inputId = "go", label = "Start"), #, onclick = "$('#my-modal').modal().focus();"
  shinyWidgets::progressBar(id = "pb", value = 0, display_pct = TRUE),
  selectInput('letters', 'choose', letters, multiple = TRUE)
)

server <- function(input, output, session) {
  count <- reactiveVal()
  
  observeEvent(input$go, {
    
    shinyWidgets::updateProgressBar(session = session, id = "pb", value = 0) # reinitialize to 0 if you run the calculation several times
    
    for (i in 1:10) {
      
      count(i)
      
      Sys.sleep(.5)
      
    }
    
  })
  
  observe({
    
    updateProgressBar(session = session, id = "pb", value = 100/10 * count())
    
    updateSelectInput(session, 'letters', selected = letters[count()])
  })
  
}

shinyApp(ui = ui, server = server)

And the removeUI use sendRemoveUI to fire message. If immediate == FALSE, it use onFlushed which make the process depend on reactive process. Hope the above can help.

1 Like

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