3

See the following nonsense minimal-working example that resembles a problem:

library(shiny)
ui = shiny::fluidPage(

    shiny::sidebarLayout(
        shiny::sidebarPanel(
            shiny::radioButtons("type", "Type", choices = c("5", "15", "21")),
            shiny::sliderInput("x", "x", min = 1, max = 10, value = 7),
            shiny::checkboxInput("auto","Set y to 8 when type is 15, and 9 when type is 21", value = TRUE),
            shiny::sliderInput("y", "y", min = 1, max = 10, value = 7)
        ),

        shiny::mainPanel(
            shiny::plotOutput("show")
        )
    )
)
server = function(input, output, session) {
    plotData = shiny::reactive({
        list(x = input$x, y = input$y, type = input$type)
    })

    output$show = renderPlot({
        shiny::req(plotData())
        d = plotData()
        print("refresh")
        plot(d$x,d$y, pch = as.integer(d$type))
    })


    observe({
        n = input$n
        a = input$auto
        type = input$type

        if (type == "a") return(NULL)
        if (a) shiny::updateSliderInput(session, "y", value = switch(type, "15" = 8, "21" = 9))
    })
}
shiny::shinyApp(ui = ui, server = server)

The problem is that the plot is rendered twice when the user selects either type 15 or 21. That is caused by the fact that when plotData changes, updateSliderInput is executed before renderPlot. (I have seen this behavior with the reactlog package.) However, I don't know how to solve this. I've tried a bunch of functions, such as isolate, req, but without success.

Martijn Tennekes
  • 1,951
  • 13
  • 19

1 Answers1

3

To avoid triggering reactives or outputs unnecessarily you should almost alway use freezeReactiveValue when using a update* function in :

library(shiny)

ui = shiny::fluidPage(
  shiny::sidebarLayout(
    shiny::sidebarPanel(
      shiny::radioButtons("type", "Type", choices = c("5", "15", "21")),
      shiny::sliderInput("x", "x", min = 1, max = 10, value = 7),
      shiny::checkboxInput("auto","Set y to 8 when type is 15, and 9 when type is 21", value = TRUE),
      shiny::sliderInput("y", "y", min = 1, max = 10, value = 7)
    ),
    shiny::mainPanel(
      shiny::plotOutput("show")
    )
  )
)

server = function(input, output, session) {
  plotData = shiny::reactive({
    list(x = input$x, y = input$y, type = input$type)
  })
  
  observe({
    n = input$n
    a = input$auto
    type = input$type
    
    if (type == "a") return(NULL)
    freezeReactiveValue(input, "y")
    if (a) shiny::updateSliderInput(session, "y", value = switch(type, "15" = 8, "21" = 9))
  }, priority = 1)
  
  output$show = renderPlot({
    shiny::req(plotData())
    d = plotData()
    print("refresh")
    plot(d$x,d$y, pch = as.integer(d$type))
  })
}

shiny::shinyApp(ui = ui, server = server)

Please see this related chapter from Mastering Shiny.

ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
  • Thanks! I noticed that it only worked when the `observe` chunk is placed above `renderPlot`. – Martijn Tennekes Feb 19 '22 at 21:07
  • 1
    That is just to get the initialization of the app right. You can achive the same by increasing the observers `priority = 1` (if you want to keep the order of the code). Personally I'd prefer setting the output at the end of the script. – ismirsehregal Feb 19 '22 at 21:15
  • @MartijnTennekes another comment on your code: you should be aware of the fact that using `::` costs time - not much but it adds up. Please see [this](https://stackoverflow.com/a/23232888/9841389). – ismirsehregal Feb 20 '22 at 14:51
  • I will use this code for an upcoming CRAN package. I will put shiny into the "suggests" lists to minimize the dependency overload, and place the app in a `if (requireNamespace("shiny")) {}` statement. Do you know if CRAN accepts function calls without `::`? – Martijn Tennekes Feb 20 '22 at 21:20
  • I'll stay with the double-column. Do you perhaps know the answer to https://stackoverflow.com/questions/71300472/shiny-how-to-fix-sidebarpanel-when-mainpanel-is-scrollable? – Martijn Tennekes Mar 01 '22 at 09:03