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.
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.
A restore is a transaction between the server and the client script that shinysnap adds to the page.
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.receiveMessage(), and keeps the
others pending.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.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.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.
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:
#> <shinysnap_report> 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:
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.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.snap_restore() returns immediately with a handle; the
report arrives later. Three ways to use it:
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.
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:
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.