jsplyr keeps your data in the browser, so
collect() has to fetch it back over an asynchronous
round-trip. Because of that, collect() returns a promise, not a data
frame.
Reactive outputs such as shiny::renderTable()
or DT::renderDT() understand promises and resolve them for
you, which is why a plain collect() inside a render
function “just works”:
Every other reactive context — reactive(),
eventReactive(), observeEvent() and
observe() — hands you the promise as-is. The value resolves
later, so you cannot use the result of collect()
synchronously on the next line; treating it as a data frame directly
gives you a promise object instead of your rows. You handle it with
promises::then() or the re-exported %...>%
pipe.
Both of these work in any context — reactive(),
eventReactive(), observeEvent() and
observe() alike. Pick whichever reads better. The examples
below use the %...>% pipe.
promises::then()then() registers a callback that runs once the data
arrives. It returns a new promise, so it composes:
%...>% pipeThe promises package ships a “promise pipe”,
%...>%, that pipes the resolved value into the
next expression. It reads just like a regular pipe but waits for the
promise to settle first. jsplyr re-exports it, so you do
not have to import promises yourself:
Use %...!% to handle errors from the promise chain:
A common task is to set the value of an input — say a
selectInput — from a number computed in the browser.
Functions like shiny::updateSelectInput() are side
effects, not reactive outputs. They do not understand promises,
so you cannot pass collect()’s result straight to
selected — you would hand it a promise object instead of
your value. Resolve the promise first and act on the value once it
arrives.
The cleanest approach keeps the updateSelectInput() call
outside the jsplyr pipeline. Store the
resolved value in a reactiveVal and let a separate observer
update the input. This decouples “compute the value” from “update the
input”:
# Holds the value computed in the browser.
oldest_age <- shiny::reactiveVal(NULL)
# Pipeline: compute max(age) and store it. No UI update here.
shiny::observeEvent(input$update, {
lazy_data() |>
dplyr::summarise("max_age = max(age)") |>
dplyr::collect() %...>% {
oldest_age(.$max_age)
}
})
# Separate observer: update the input when the computed value changes.
shiny::observeEvent(oldest_age(), {
shiny::updateSelectInput(
session,
inputId = "age",
selected = oldest_age()
)
})The %...>% pipe waits for the collected value to
arrive, then . holds the result tibble so
.$max_age is written into the reactiveVal. The
second observer reacts to that change and performs the update.
If you prefer to keep the computation in a reactive expression
instead of an observer, return the collected promise from an
eventReactive() (gated on the button) and resolve it in a
separate observe():
# Gated on the button: returns the collect() promise. Computes only.
oldest_age <- shiny::eventReactive(input$update, {
lazy_data() |>
dplyr::summarise("max_age = max(age)") |>
dplyr::collect()
})
# Separate observer: resolve the promise and update the input.
shiny::observe({
oldest_age() %...>% {
shiny::updateSelectInput(
session,
inputId = "age",
selected = .$max_age
)
}
})eventReactive() returns the promise as-is, so
oldest_age() is a promise; the observe()
resolves it with %...>% and updates the input. A plain
reactive() works the same way if you want the value
recomputed whenever its dependencies change rather than only on a button
press.
See inst/example_apps/app_update_select.R for a complete
runnable app showing both approaches.