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)
One of simplest solutions can be:
just change your observeEvent(input$Select1, ... observer to this one
once_flag <- reactiveVal(TRUE)
observeEvent(input$Select1, {
updateSelectizeInput(
session, inputId = "Select2", server = TRUE,
choices = dat$Letter[dat$Number %in% input$Select1],
selected = if(once_flag()) input$store$Select2 else NULL
)
once_flag(FALSE)
}, ignoreInit = TRUE)
And you are done. How simple it is! Everything is solved in the same observeEvent, with only one call of update. The once_flag is to make sure setting Select2 value only one time. Second time and on when you change Select1, we don't set the value of Select2.
Thanks for the correction from other users like @ismirsehregal, so I can come up with the simple solution above. Since shinyStore has provided you this wrapper API directly by input$store, you don't need to write the API as I did below, but the workflow and what's behind are the same. If you are interested in how this solution work, continue to read.
The biggest problem we want to solve is server = TRUE, read the help file we know that store choices on the server-side, and load the select options dynamically on searching. That means your client (UI) doesn't know what are the options at the start. HTML5 localstore (what behind shinystore) is a client-side technology, it can only change things that exist at start. If the options are not given when you launch the app, it can't change it. That's why it failed.
If the select2 is updated after/based on select1, can we retrieve the value from shinystore after we settle select1 and then assign the value to select2?
The answer is no and yes. No because the original (not true, see comments, but below is helpful to understand how shinystore works). Yes, is because if you understand how html5 localstorage and how Shiny's JS-R communicate, we can write our own API to get the value.shinystore didn't provide you any API for R-Javascript communication to retrieve the values. It only allows to set, not get
Here is the workflow:
select1select1 updated, update select2's optionsselect2 and send from JS to R. (In shinyStore you can access it by input$store$xxx (input ID). Below, I manually write the code out to show you how they return client-side value into input value.)select2's selected choiceLet's see how it works in code:
observeEvent(input$Select1, {
# detect 1 changed
# send signal to client to get stored 2's value
session$sendCustomMessage(
"shinyStore_getvalue",
list(
namespace = "shinyStore-ex1",
key = "Select2"
)
)
# updated 2's choices based on 1
updateSelectizeInput(session, inputId = "Select2",
choices = dat$Letter[dat$Number %in% input$Select1],
server = TRUE)
}, ignoreInit = TRUE)
How R-JS communicate, read this page.
Shiny.addCustomMessageHandler('shinyStore_getvalue', function(data) {
var val = localStorage.getItem(`${data.namespace}\\${data.key}`);
if(val === null) return false;
val = JSON.parse(val);
if(val.data === undefined) return false;
Shiny.setInputValue(`shinystore_${data.key}`, val.data);
});
get the queried shinystore value and send it to R shiny as an input value that can be directly observe. Not explain details here, again, read that link above if you want to know more.
observeEvent(input$shinystore_Select2, {
updateSelectizeInput(
session,
inputId = "Select2",
choices = dat$Letter[dat$Number %in% input$Select1],
server = TRUE,
selected = input$shinystore_Select2
)
}, once = TRUE)
Add once for only setting the value one time.
Full code for the detailed solution
library(shiny)
library(shinyStore)
ui <- fluidPage(
headerPanel("shinyStore Example"),
tags$script(HTML(
'
Shiny.addCustomMessageHandler(\'shinyStore_getvalue\', function(data) {
var val = localStorage.getItem(`${data.namespace}\\\\${data.key}`);
if(val === null) return false;
val = JSON.parse(val);
if(val.data === undefined) return false;
Shiny.setInputValue(`shinystore_${data.key}`, val.data);
});
'
)),
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, {
# detect 1 changed
# send signal to client to get stored 2's value
session$sendCustomMessage(
"shinyStore_getvalue",
list(
namespace = "shinyStore-ex1",
key = "Select2"
)
)
# # updated 2's choices based on 1
updateSelectizeInput(session, inputId = "Select2",
choices = dat$Letter[dat$Number %in% input$Select1],
server = TRUE)
}, ignoreInit = TRUE)
observeEvent(input$shinystore_Select2, {
updateSelectizeInput(
session,
inputId = "Select2",
choices = dat$Letter[dat$Number %in% input$Select1],
server = TRUE,
selected = input$shinystore_Select2
)
}, once = 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With