Skip to content

fix: debounce shiny:conditional to fire once per frame#4374

Draft
cpsievert wants to merge 1 commit into
mainfrom
fix/shiny-conditional-multiple-fires
Draft

fix: debounce shiny:conditional to fire once per frame#4374
cpsievert wants to merge 1 commit into
mainfrom
fix/shiny-conditional-multiple-fires

Conversation

@cpsievert
Copy link
Copy Markdown
Collaborator

Summary

Fixes #3668.

$updateConditionals() — which triggers the shiny:conditional event and re-evaluates all [data-display-if] elements — is called from 4 places: connect(), reconnect(), sendInput(), and dispatchMessage(). During a typical user interaction (e.g. clicking a button), it fires at least twice per cycle: once from sendInput() when the input is sent, and again from dispatchMessage() when the server response arrives. Server-driven inputMessages can add yet another call.

This PR debounces $updateConditionals() using requestAnimationFrame, so multiple calls within a single animation frame collapse into one evaluation. This is safe because:

  • None of the 4 call sites depend on synchronous completion (each call is the last statement in its block)
  • requestAnimationFrame callbacks run before the browser paints, so there's no visual flicker
  • The evaluation always reads the latest $inputValues and $values at execution time, so deferring doesn't lose data

Test plan

  • Run the repro from shiny:conditional is running multiple times during the flush cycle #3668 and verify shiny:conditional fires once per interaction instead of multiple times:
    library(shiny)
    ui <- fluidPage(
      tags$head(
        tags$script("$(document).on('shiny:conditional', function(event) {console.log(event);})")
      ),
      actionButton("val", "Value"),
      textOutput("out")
    )
    server <- function(input, output, session) {
      output$out <- renderText({ input$val })
    }
    shinyApp(ui, server)
  • Verify conditionalPanel() still works (elements show/hide correctly based on input conditions)

`$updateConditionals()` is called from `connect()`, `reconnect()`,
`sendInput()`, and `dispatchMessage()`. During a typical interaction
it fires at least twice per cycle (e.g. sendInput + dispatchMessage),
redundantly re-evaluating all `[data-display-if]` elements each time.

Debounce via `requestAnimationFrame` so multiple calls within a single
frame collapse into one evaluation. None of the call sites depend on
synchronous completion, and rAF runs before the browser paints, so
there is no visual flicker.
@cpsievert cpsievert marked this pull request as draft April 28, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shiny:conditional is running multiple times during the flush cycle

1 participant