'How to prevent user from doing anything on shiny app when app is busy

I have a complex shiny app with a lot of inputs, a leaflet map etc ... The problem I have is when the app is busy making some calculations, the users keep on clicking anywhere on the app, and sometimes the app crash.

I would like to prevent the users from making any click when the app is busy. It is important that the spinner stays a spinner and not a full page waiter like in the waiter package. Maybe there is a possibility to combine a spinner and waiter ? But I did'nt find out yet how to.

I have a small reprex here : When i click one the button "busy app", there is a 5 seconds spinner to make the user wait. But during this time, the user can still click on "increment" button. And at the end of the spinner, the output increments by the number of clicks that where made. I would like to block the whole app to the user at once, not just put a "disable" on the button when the app is busy (because I have lots of input in my app and that would require too much modifications)

library(shiny)

ui <- fluidPage(

  # spinner css
  tags$head(
    tags$style(HTML("
  #loadmessage {
  position:fixed; z-index:8; top:50%; left:50%; padding:10px;
  text-align:center; font-weight:bold; color:#000000; background-color:#CCFF66;
  }
  
  .loader {
  position:fixed; z-index:8; border:16px solid #999999;
  border-top: 16px solid #8B0000; border-radius: 50%;
  width: 120px; height: 120px; top:45%; left:45%;
  animation: spin 2s linear infinite;
  }

  @keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }"))
  ),
  
  # display load spinner when shiny is busy
  conditionalPanel(
    condition = "$(\'html\').hasClass(\'shiny-busy\')",
    tags$div(class = "loader")
  ),
  actionButton(
    inputId = "increment",
    label = "Increment"
  ),
  textOutput("result"),
  actionButton(
    inputId = "busy",
    label = "Busy app"
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(counter = 0)

  #increment counter
  observeEvent(input$increment,{
    rv$counter = rv$counter + 1
  })
  
  #display incremented counter
  output$result <- renderText({
    rv$counter
  })
  
  observeEvent(input$busy, {
    Sys.sleep(5)
    # during this time, the user should not be able to do anything on the app
  })
}

shinyApp(ui = ui, server = server)


Solution 1:[1]

After analyzing yours answers and think more about it, I think I found the simpliest solution.

On "shiny busy" event, I display a div in the conditional panel which is 100% of the screen and on first plan, so it prevents any click on the inputs / outputs behind it. When the app is not busy anymore, the panel disappear. The panel is transparent so the user doesn't see it.

Also, it enables me to disable all inputs and output without being dependant of a timer, only on if the app is busy or not.

library(shiny)

ui <- fluidPage(

  # spinner css
  tags$head(
    tags$style(HTML("
  #loadmessage {
  position:fixed; z-index:8; top:50%; left:50%; padding:10px;
  text-align:center; font-weight:bold; color:#000000; background-color:#CCFF66;
  }
  
  .loader {
  position:fixed; z-index:8; border:16px solid #999999;
  border-top: 16px solid #8B0000; border-radius: 50%;
  width: 120px; height: 120px; top:45%; left:45%;
  animation: spin 2s linear infinite;
  }

  .prevent_click{
  position:fixed; 
  z-index:9;
  width:100%;
  height:100vh;
  background-color: transpare'nt;   
  }

  @keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }"))
  ),
  
  # display load spinner when shiny is busy
  conditionalPanel(
    condition = "$(\'html\').hasClass(\'shiny-busy\')",
    tags$div(class = "loader"),
    tags$div(class = "prevent_click")
  ),
  actionButton(
    inputId = "increment",
    label = "Increment"
  ),
  textOutput("result"),
  actionButton(
    inputId = "busy",
    label = "Busy app"
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(counter = 0)

  #increment counter
  observeEvent(input$increment,{
    rv$counter = rv$counter + 1
  })
  
  #display incremented counter
  output$result <- renderText({
    rv$counter
  })
  
  observeEvent(input$busy, {
    Sys.sleep(5)
    # during this time, the user should not be able to do anything on the app
  })
}

shinyApp(ui = ui, server = server)

Solution 2:[2]

We can try disabling all inputs using shinyjs library combined with walk function.

  observeEvent(input$busy, {
    names(input) %>% walk(disable)
    Sys.sleep(3) 
    # during this time, the user should not be able to do anything on the app
    names(input) %>% walk(enable)
  })
}

app:

library(shiny)
library(shinyjs)
library(tidyverse)

ui <- fluidPage(
  useShinyjs(),
  # spinner css
  tags$head(
    tags$style(HTML("
  #loadmessage {
  position:fixed; z-index:8; top:50%; left:50%; padding:10px;
  text-align:center; font-weight:bold; color:#000000; background-color:#CCFF66;
  }
  
  .loader {
  position:fixed; z-index:8; border:16px solid #999999;
  border-top: 16px solid #8B0000; border-radius: 50%;
  width: 120px; height: 120px; top:45%; left:45%;
  animation: spin 2s linear infinite;
  }

  @keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }"))
  ),
  
  # display load spinner when shiny is busy
  conditionalPanel(
    condition = "$(\'html\').hasClass(\'shiny-busy\')",
    tags$div(class = "loader")
  ),
  actionButton(
    inputId = "increment",
    label = "Increment"
  ),
  textOutput("result"),
  actionButton(
    inputId = "busy",
    label = "Busy app"
  )
)

server <- function(input, output, session) {
  
  rv <- reactiveValues(counter = 0)
  
  #increment counter
  observeEvent(input$increment,{
    rv$counter = rv$counter + 1
  })
  
  #display incremented counter
  output$result <- renderText({
    rv$counter
  })
  
  observeEvent(input$busy, {
    names(input) %>% walk(disable)
    Sys.sleep(3) 
    # during this time, the user should not be able to do anything on the app
    names(input) %>% walk(enable)
  })
}

shinyApp(ui = ui, server = server)

Solution 3:[3]

I would also use shinyjs, but instead of going through all inputs, I would use some CSS and change the pointer-events of all inputs and buttons. You can adjust the CSS-selector to your needs.

  observeEvent(input$busy, {
    shinyjs::runjs('$("input, button").css("pointer-events", "none")')
    Sys.sleep(5)
    # during this time, the user should not be able to do anything on the app
    shinyjs::runjs('$("input, button").css("pointer-events", "unset")')
  })

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 gdevaux
Solution 2
Solution 3 SeGa