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)
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)
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):
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):
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
.