Search code examples
rshinyshinystore

shinyStore cannot restore the selected values of the selectizeInput if the choices depend on another input and server = TRUE


This is a follow-up question from this question (shinyStore cannot restore the selected values of the selectizeInput if the choices are depends on another input) I asked before. I have figured out the answer (https://stackoverflow.com/a/68290227/7669809). However, now I realized that my answer is not complete. Please see the following code. This is the same as my previous question and answer, except that I set server = TRUE for the first updateSelectizeInput, which makes the local storage not working. It would be great if I could use server = TRUE because in my real-world example the choices of my selectizeInput are a lot.

### This script creates an example of the shinystore package

# Load packages
library(shiny)
library(shinyStore)

ui <- fluidPage(
  headerPanel("shinyStore Example"),
  sidebarLayout(
    sidebarPanel = sidebarPanel(
      initStore("store", "shinyStore-ex1"),
      selectizeInput(inputId = "Select1", label = "Select A Number",
                     choices = as.character(1:3),
                     options = list(
                       placeholder = 'Please select a number',
                       onInitialize = I('function() { this.setValue(""); }'),
                       create = TRUE
                     ))
    ),
    mainPanel = mainPanel(
      fluidRow(
        selectizeInput(inputId = "Select2", 
                       label = "Select A Letter",
                       choices = character(0),
                       options = list(
                         placeholder = 'Please select a number in the sidebar first',
                         onInitialize = I('function() { this.setValue(""); }'),
                         create = TRUE
                       )),
        actionButton("save", "Save", icon("save")),
        actionButton("clear", "Clear", icon("stop"))
      )
    )
  )
)

server <- function(input, output, session) {
  
  dat <- data.frame(
    Number = as.character(rep(1:3, each = 3)),
    Letter = letters[1:9]
  )
  
  observeEvent(input$Select1, {
    updateSelectizeInput(session, inputId = "Select2", 
                         choices = dat$Letter[dat$Number %in% input$Select1],
                         # Add server = TRUE make the local storage not working
                         server = TRUE)
  }, ignoreInit = TRUE)
  
  observe({
    if (input$save <= 0){
      updateSelectizeInput(session, inputId = "Select1", selected = isolate(input$store)$Select1)
    }
  })
  
  observe({
    if (input$save <= 0){
      req(input$Select1)
      updateSelectizeInput(session, inputId = "Select2", selected = isolate(input$store)$Select2)
    }
  })
  
  observe({
    if (input$save > 0){
      updateStore(session, name = "Select1", isolate(input$Select1))
      updateStore(session, name = "Select2", isolate(input$Select2))
    }
  })

  observe({
    if (input$clear > 0){
      updateSelectizeInput(session, inputId = "Select1",
                           options = list(
                             placeholder = 'Please select a number',
                             onInitialize = I('function() { this.setValue(""); }'),
                             create = TRUE
                           ))
      updateSelectizeInput(session, inputId = "Select2",
                           choices = character(0),
                           options = list(
                             placeholder = 'Please select a number in the sidebar first',
                             onInitialize = I('function() { this.setValue(""); }'),
                             create = TRUE
                           ))

      updateStore(session, name = "Select1", NULL)
      updateStore(session, name = "Select2", NULL)
    }
  })
}

shinyApp(ui, server)

Solution

  • We don't have to use custom JavaScript or add further dependencies to solve this problem - input$store is shinyStore's built-in way to retrieve data from the localStorage object and provides us with all needed information on session start (and it is already being used by @www in the example code).

    The session object in shiny provides the server (among other things) with client side (or browser) information - e.g. session$clientData$url_search or of interest here: session$input$store.

    We have to make sure, that the selection we are trying to set is available in the choices when using updateSelectizeInput - e.g. something like this:

    updateSelectizeInput(session, inputId = "myID", selected = 12, choices = 1:10)

    won't work.

    Furthermore, we need to use freezeReactiveValue to stop triggering other observers downstream after restoring on session start to avoid overwriting the update again.

    freezeReactiveValue btw. is almost alway applicable when using update* functions in shiny. Please see this related chapter in Mastering Shiny.

    ### This script creates an example of the shinystore package
    
    # Load packages
    library(shiny)
    library(shinyStore)
    
    ui <- fluidPage(
      headerPanel("shinyStore Example"),
      sidebarLayout(
        sidebarPanel = sidebarPanel(
          initStore("store", "shinyStore-ex1"),
          selectizeInput(inputId = "Select1", label = "Select A Number",
                         choices = as.character(1:3),
                         options = list(
                           placeholder = 'Please select a number',
                           onInitialize = I('function() { this.setValue(""); }'),
                           create = TRUE
                         ))
        ),
        mainPanel = mainPanel(
          fluidRow(
            selectizeInput(inputId = "Select2", 
                           label = "Select A Letter",
                           choices = character(0),
                           options = list(
                             placeholder = 'Please select a number in the sidebar first',
                             onInitialize = I('function() { this.setValue(""); }'),
                             create = TRUE
                           )),
            actionButton("save", "Save", icon("save")),
            actionButton("clear", "Clear", icon("stop"))
          )
        )
      )
    )
    
    server <- function(input, output, session) {
      
      dat <- data.frame(
        Number = as.character(rep(1:3, each = 3)),
        Letter = letters[1:9]
      )
      
      storeInit <- observeEvent(input$store, {
        freezeReactiveValue(input, "Select1") # required
        freezeReactiveValue(input, "Select2") # not required but should be used before calling any update function which isn't intended to trigger further reactives
        updateSelectizeInput(session, inputId = "Select1", selected = input$store$Select1)
        updateSelectizeInput(session, inputId = "Select2", selected = input$store$Select2, choices = dat$Letter[dat$Number %in% input$store$Select1], server = TRUE)
        storeInit$destroy() # destroying observer, as it is only needed once per session
      }, once = TRUE, ignoreInit = FALSE)
      
      observeEvent(input$Select1, {
        freezeReactiveValue(input, "Select2") # not required but good practice
        updateSelectizeInput(session, inputId = "Select2", 
                             choices = dat$Letter[dat$Number %in% input$Select1],
                             server = TRUE)
      }, ignoreInit = TRUE)
      
      observe({
        if (input$save > 0){
          updateStore(session, name = "Select1", isolate(input$Select1))
          updateStore(session, name = "Select2", isolate(input$Select2))
        }
      })
      
      observe({
        if (input$clear > 0){
          freezeReactiveValue(input, "Select1") # not required but good practice
          freezeReactiveValue(input, "Select2") # not required but good practice
          updateSelectizeInput(session, inputId = "Select1",
                               options = list(
                                 placeholder = 'Please select a number',
                                 onInitialize = I('function() { this.setValue(""); }'),
                                 create = TRUE
                               ))
          updateSelectizeInput(session, inputId = "Select2",
                               choices = character(0),
                               options = list(
                                 placeholder = 'Please select a number in the sidebar first',
                                 onInitialize = I('function() { this.setValue(""); }'),
                                 create = TRUE
                               ))
          
          updateStore(session, name = "Select1", NULL)
          updateStore(session, name = "Select2", NULL)
        }
      })
    }
    
    shinyApp(ui, server)
    

    result


    Edit: comparison of the answers given

    Now that @lz100 is also using input$store instead of Shiny.addCustomMessageHandler both answers are approximating each other. It boils down to the use of a reactiveVal in @lz100's updated answer (once_flag) and the use of freezeReactiveValue in my answer.

    I'd like to point out why I think using freezeReactiveValue is the cleaner approach:

    The once_flag-approach fires after input$Select1 is updated (observeEvent parameter ignoreInit = TRUE) and is indirectly depending on input$store. All other observers depending on input$Select1 are unnecessarily triggered twice (first on init, second on update).

    Here is the according reactlog (0.0321s to first idle):

    reactlog_once_flag

    Another flaw of the once_flag-approach (as it currently stands) is that the observeEvent will fire everytime input$Select1 is changed, even though no restoring is ongoing (returning NULL but wasting resources).

    The freezeReactiveValue-approach is directly listening to changes of input$store when first invoking the app (once = TRUE, ignoreInit = FALSE) preventing downstream triggers, which is slightly faster (0.0212s to first idle):

    reactlog_freezeReactiveValue

    With a growing app these effects may become more relevant regarding the initialization time - accordingly I second the recommendation I linked above to pair update* functions with freezeReactiveValue.