--- title: "Restoring dynamic UI" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Restoring dynamic UI} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` This vignette explains why restoring state into an app with `renderUI()` is hard, what shinysnap does about it, and how to read the report that every restore produces. ## Why hand-rolled restores need delays `session$sendInputMessage(id, list(value = v))` is what every `update*Input()` function calls. On the client, Shiny looks for a *bound* input with that id and, if it finds none, drops the message. Nothing is logged on either side. An input that lives inside a `renderUI()` which has not rendered yet is exactly such an input, so a restore that sends every value at once loses the values of all dynamic inputs. The usual workaround is to send those values later, after a guessed delay, one wave per level of dynamic UI. The guesses are fragile, and a wave that arrives too early is still dropped silently. ## What shinysnap does instead A restore is a *transaction* between the server and the client script that shinysnap adds to the page. 1. The server writes the tracked values back into your `reactiveValues`, runs the `snap_on_restore()` hooks, and sends **all** input values to the browser in one message, each already turned into the payload its input binding understands. 2. The client applies every value whose input is on the page right away, through the binding's `receiveMessage()`, and keeps the others pending. 3. Whenever Shiny binds a new input (because a `uiOutput` rendered or re-rendered), the client checks the pending list and applies the value for that id. The value of the select that controls a branch is applied first, the branch renders, its inputs bind, their values are applied, a nested branch renders, and so on, without any timing configuration. 4. While the transaction is in flight, the server also primes shiny's own `restoreInput()` mechanism with the snapshot's values. Every built-in input constructor calls `restoreInput()`, so dynamic UI that renders during the restore is built with the restored value *in the HTML*: no flash of defaults, and observers watching those inputs fire once, with the right value. The client recognizes such inputs and reports them as `constructed` rather than applying the value a second time. 5. The transaction settles once the page has been quiet for a moment (no busy state, no new UI, no new values, 0.3 seconds by default) or after the timeout (10 seconds by default). The client then reports one status per input. One detail matters for anyone who has tried to do this themselves: Shiny sends a newly bound input's *initial* value to the server right after it fires the bound event. A value applied synchronously from that event is overwritten by the default a moment later. shinysnap defers each apply past that point. ## The report `snap_restore()` returns a handle whose promise resolves to a report, and `snap_on_restored()` hooks receive the same report. It is a data frame with one row per input: ``` #> 6 input(s), settled after 0.41 s #> applied: 2, constructed: 3, missing: 1 #> id status binding detail #> method applied shiny.selectInput #> b_k constructed shiny.numberInput #> b_text constructed shiny.textInput #> shared constructed shiny.numberInput #> a_rate applied shiny.sliderInput #> ghost missing ``` | status | meaning | |---|---| | `applied` | sent to an input that was on the page | | `constructed` | the input appeared during the restore already carrying the value (via `restoreInput()`) | | `reapplied` | the input re-rendered during the restore and received the value again (only without the accelerator) | | `missing` | the input never appeared before the restore settled | | `failed` | the binding raised an error; `detail` has the message | | `mismatched` | applied, but the input reports a different value afterwards, for example a select whose choices do not contain it | | `skipped` | excluded, or its restorer chose not to restore it (passwords, buttons, uploads) | The attributes `txn`, `elapsed`, `settled`, and `timed_out` describe the transaction. `missing` and `failed` inputs produce one consolidated warning by default; `snap_restore(unknown = "skip")` silences it and `unknown = "error"` rejects the promise instead. A `missing` row is the normal outcome for an input that a newer version of the app no longer has, or for an input whose UI is not reachable in the restored state. Two situations are worth knowing about: - Outputs on hidden tabs are suspended by default, so a `renderUI()` on a tab the user is not looking at does not render and its inputs stay `missing`. Use `outputOptions(output, "id", suspendWhenHidden = FALSE)` for outputs a restore must reach, or include the tab's id in the snapshot so the restore switches to it. - Inputs created only from JavaScript with `Shiny.setInputValue()` (plot clicks, table selections) are not bound inputs and cannot be restored through a binding; they are not captured in the first place. ## Working with the promise `snap_restore()` returns immediately with a handle; the report arrives later. Three ways to use it: ```{r, eval = FALSE} observeEvent(input$go, { # 1. A callback snap_restore(input$json, on_done = function(report) print(report)) # 2. A hook that sees every restore in the session snap_on_restored(function(state, report) message(nrow(report), " inputs")) # 3. The promise itself handle <- snap_restore(input$json) promises::then(handle$promise, function(report) print(report)) NULL }) ``` The handle is deliberately *not* a promise. Shiny waits for a promise that an observer returns before it flushes, and the report can only arrive once the browser has seen the page go quiet, so an observer that returned the promise would stall its own restore. Keep promises inside the observer, or end the observer with `NULL` as above when its last expression is a `promises::then()` call. ## Reacting during a restore `snap_is_restoring()` is `TRUE` from the moment `snap_restore()` is called until the report arrives. Use it to keep expensive observers quiet while intermediate values stream in, and to run something once the state is complete: ```{r, eval = FALSE} observeEvent(input$n, { if (snap_is_restoring()) { return() } fit() }) observe({ if (!snap_is_restoring()) fit() }) ``` ## Notes on timing - Inputs with a rate policy (text inputs debounce, sliders throttle) may lag the browser by a few hundred milliseconds. A snapshot taken from a download handler runs after the click has reached the server, which in practice is later than that; do not add delays. - A second `snap_restore()` while one is in flight cancels the first, whose promise rejects with a condition of class `shinysnap_cancelled`. - `snap_restore(use_restore_context = FALSE)` turns the `restoreInput()` accelerator off. Everything still ends in the right state; dynamic inputs are then `applied` or `reapplied` instead of `constructed`, and their observers see the default value before the restored one. It exists to isolate problems, not for regular use.