Repository navigation
Refreshable reactivePoll #4369
Description
In my Shiny applications, I often encounter the need to keep a reactive object (e.g., a table) in sync with an external data source over time, while also updating it immediately after a user-triggered change.
A typical example is a database table: I want to display an up-to-date version of the data to the user. For this purpose, Shiny already provides reactivePoll(), which allows periodic checks for updates and refreshes the reactive accordingly.
However, in most real-world scenarios where I use reactivePoll(), I also need a mechanism to trigger an immediate refresh after a user modifies the underlying data. For instance, if a user inserts a new record into the database, they naturally expect to see it reflected in the UI right away.
One possible approach is to set a very short polling interval, but this is often not feasible. In practice, multiple users may be connected to the same worker, and frequent polling can lead to excessive database queries and performance degradation (I have personally experienced this causing the app to freeze). For this reason, I typically use a relatively long polling interval (e.g., ~100 seconds), which balances freshness and performance.
The downside is that, with this setup, a user may need to wait over a minute to see changes they just made, resulting in a poor user experience and a perception that the app is unresponsive.
To address this, I have repeatedly implemented workarounds that combine:
- periodic polling (at a reasonable interval), and
- a manual trigger to refresh the reactive immediately after a user update.
Conceptually, what I need is a refreshable reactivePoll(): a version that can be invalidated programmatically, without waiting for the next polling cycle.
As a proof of concept, I implemented a small extension of reactivePoll() that returns a list instead of a single reactive:
value: the reactive expression (same as standard reactivePoll())refresh(): a function that forces a refresh by updating the internal “cookie” used for change detection
Here is the implementation:
refreshableReactivePoll <- function(
intervalMillis,
session,
checkFunc,
valueFunc
) {
intervalMillis <- shiny:::coerceToFunc(intervalMillis)
rv <- reactiveValues(cookie = isolate(checkFunc()))
re_finalized <- FALSE
env <- environment()
o <- observe({
if (re_finalized) {
o$destroy()
rm(o, envir = env)
return()
}
rv$cookie <- checkFunc()
invalidateLater(intervalMillis(), session)
})
re <- reactive(
{
rv$cookie
valueFunc()
},
label = NULL
)
reg.finalizer(attr(re, "observable"), function(e) {
re_finalized <<- TRUE
})
re <- list(
value = re,
refresh = function() {
rv$cookie <- checkFunc()
}
)
on.exit(rm(re))
return(re)
}
Updating the “cookie” rather than the reactive value directly avoids redundant updates when the next polling cycle runs, since the state is already aligned. This allows the reactive to be refreshed both periodically and on demand (e.g., immediately after a database write).
I am not aware of an officially recommended pattern for this use case, but this need arises frequently in my applications. It feels like Shiny currently lacks a clean, built-in way to handle this pattern efficiently and systematically.
I believe that adding native support for a programmatically refreshable reactivePoll() (or a similar abstraction) would be a valuable enhancement for many real-world applications.
Activity
cpsievert commentedon Apr 1, 2026
Thanks for the well-written issue, @Upipa — this is a common and legitimate pattern.
The good news is that this already works with the existing
reactivePoll()API, no changes needed. The key insight is thatcheckFuncruns inside a reactive context (anobserve), so if it reads a reactive value, changing that value will immediately re-trigger the check (and thus refresh the data) without waiting for the next polling cycle.Here's the pattern:
When
refresh_trigger()changes, the observer that drivesreactivePollre-executescheckFunc()immediately. The new check value updates the internal cookie, which invalidates the downstream reactive — giving you the instant refresh you're after. The periodic polling continues to work as usual for changes made by other users/processes.This is admittedly not obvious from the docs, so we could improve the
?reactivePolldocumentation to mention this pattern. But I don't think a new function or API change is warranted here since the existing mechanism handles it cleanly.gadenbuie commentedon Apr 1, 2026
lol I also took a pass at this and completely missed this detail. We should definitely mention this in the docs, it does not look from the outside like
checkFuncwould be evaluated in a reactive context, although it makes sense now that I've seen it!