--- title: "Working with collect() promises in reactive contexts" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Working with collect() promises in reactive contexts} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` `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](https://rstudio.github.io/promises/), 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": ```{r} output$mtcars_tb <- DT::renderDT({ lazy_mtcars_query() |> dplyr::collect() }) ``` 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. ## Two ways to unwrap a promise 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: ```{r} lazy_mtcars_query() |> dplyr::collect() |> promises::then(function(df) { # `df` is the collected tibble head(df) }) ``` ### `%...>%` pipe The `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: ```{r} lazy_mtcars_query() |> dplyr::collect() %...>% head() ``` Use `%...!%` to handle errors from the promise chain: ```{r} lazy_mtcars_query() |> dplyr::collect() %...>% head() %...!% (function(err) { shiny::showNotification(conditionMessage(err), type = "error") }) ``` ## A practical example: updating an input from a computed value 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": ```{r} # 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()`: ```{r} # 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.