updateSelectizeInput causes double-run

ggplot2
shiny

#1

Hi,
I have made a reproducible example below to show my problem.

I am using mpg dataset which contains some fuel economy data by car manufacturers and models.

I have two selectInput's each for manufacturer and model selection. I have defined the choices argument for manufacturer input in ui and I expect the list of model input could be updated depending on the value of manufacturer. And a subset of mpg table by selected manufacturer/model is shown as an output.

I have realised my app runs twice when I initialise the app or when I select a different value in manufacturer input. I insert str(input$model) before generating the underlying table to capture this phenomenon. When I initialise the app, input$model value is "" the first time and gives a blank table and only then server runs again and input$model value is updated to give a table. And if you exam the input$model value in observeEvent(filtered_models(), { before and after updateSelectizeInput, you will see input$model value stays the same and this confuses me.

The double-run is not ideal especially if some big tables with intensive calculation are needed. Will be appreciated if someone could help me with this issue.

library(shiny)
library(ggplot2) # use mpg dataset in ggplot2
library(data.table)

dt <- as.data.table(mpg)
manufacturer_list <- dt[, unique(manufacturer)]

ui <- fluidPage(
    selectInput(inputId = "manufacturer",
                label = "Manufacturer",
                choices = manufacturer_list,
                selected = manufacturer_list[1]
                ),
    selectInput(inputId = "model",
                label = "Model",
                choices = NULL
                ),
    tableOutput("table")
)

server <- function(input, output, session) {
  filtered_models <- reactive({dt[manufacturer == input$manufacturer, unique(model)]})
  
  observeEvent(filtered_models(), {
    updateSelectizeInput(session, 'model', choices = c(filtered_models()), selected = filtered_models()[1])
  })
  
  filtered_mpg <- reactive({
    # str(input$model)
    dt[manufacturer == input$manufacturer & model == input$model]
  })
  
  output$table <- renderTable(filtered_mpg())
  
}

shinyApp(ui, server)

#2

Based on my relatively limited understanding of reactivity, what I think is happening is that filtered_mpg() is running on the newly selected Manufacturer and the previously selected (i.e., not yet updated) Model, which yields an empty data frame.

You can add a req() statement that tests if input$model is returning the same vector as filtered_models(). I've included a couple of print statements to show what is happening if you run the app with the a req() statement both commented and uncommented.

  filtered_mpg <- reactive({
    req(sum(input$model %in% filtered_models()) == length(filtered_models))
    print(dt[manufacturer == input$manufacturer & model == input$model])
    print("----------------")
    dt[manufacturer == input$manufacturer & model == input$model]
  })

I'd be surprised if this is the recommended pattern for comparing these two vectors, but hopefully it points you in the right direction until another answer comes along.


#3

Thank you for your reply. I have found req() and print() very helpful debugging techniques.

I have used str() to track input/table values flowing around in shiny and sort of have narrowed down to the place causing my problem.

What I did was,

  observeEvent(filtered_models(), {
    str(input$model)
    updateSelectizeInput(session, 'model', choices = c(filtered_models()), selected = filtered_models()[1])
    str(input$model)
    browser()
  })

And when I initialise the app, str(input$model) should give me the type/value of input$model for twice in console. One before updateSelectizeInput(session, ...) and one after. What confuses me is when I initialise the app R gives me the value "" for input$model both before and after updateSelectizeInput(session, ...) where I expect it to give me "" for the first one and chr "a4" for the second one.


I also tried to use shiny reactive log to trace the app's reactivity. Here is what I think has happened after ui runs through:

  1. observeEvent(...) gets called first. observeEvent(...) starts to seek for filtered_models() in server. () gives away that filtered_models() is a reactive expression.
  2. filtered_models() gets called and input$manufacturer is the only input it needs and its value is audi, which has been defined from ui.
  3. input$manufacturer feeds into filtered_models(). filtered_models() runs and should return a list of all models under audi.
  4. observeEvent(...) is now happy with filtered_models(). It continues to run what's left in {...}. By the way, the value of input$model should be "" this time because we haven't specified the value for it (str(input$model) should give you a value of ""). Ok, updateSelectizeInput(session, ...) runs and what I expect is input$model value should now be updated to "a4", which is the first one on the list of filtered_models() under audi. However, input$model value is not updated as the second str(input$model) still returns "" to me and can also clearly see from reactivity log.
  5. Then output$table gets called. It looks for filtered_mpg() and filtered_mpg() looks for input$manufacturer and input$model. input$manufacturer's value is still "audi" but input$model is "". "" is not some model type under "audi". filtered_mpg can not find anything and table returns nothing.
  6. Up to now the first run finishes. Because updateSelectizeInput(session, ...) does not return the correct input$model value, table gives me nothing.
  7. However, after now everything done running, I see from reactive log that input$model changes its value from "" to "a4". I suspect updateSelectizeInput(session, ...) did not run until up to now. And because input$model value has changed, it triggers the second run. And this time "a4" is a model type under "audi", table can be generated.

From both ways, I sort of figure out what is happening here. But I do not know what will be a valid solution to my problem.


#4

Nice detective work on the app's reactivity. I haven't played around with the reactive log. It looks like a nice tool to help me better understand reactivity. One point of clarification about my initial reply, req is not a debugging technique, but the recommended method for handling missing inputs. Also, I have found that pairing renderUI with req often produces more predictable results than updateFooInput. The flip side of that is that updateFooInput seems to more quickly generate the widget than renderUI. But these are just my passing observations (i.e., not backed by any timings).


#5

I haven't used it, but I wonder if the cancelOutput argument to req might also be helpful in this case (see Joe Cheng's answer to a different question).


#6

Thank you very much! I think req() is the solution here. Sorry I didn't fully understand what req() was doing from your first reply. The link you provided for it above is super useful and now I know what it is doing.

My final solution which I get out of your suggestions is:

  filtered_mpg <- reactive({
    req(input$model %in% filtered_models())
    dt[manufacturer == input$manufacturer & model == input$model]
  })

as when I apply this solution to my original app, I have already restricted the multiple argument in corresponding selectInput for input$model to be FALSE.

Also thank you for the link for another similar thread. I guess I have to accept that I can't force updateSelectizeInput(session...) to update value of input$model immediately. In my original app, the req() solution does not fully avoid the double-run. But only few expression gets called twice anyway. And it is great that req() calls a stop before running the most intensive calculation like generating some underlying tables, displaying tables or plots.