Inputs created by
renderUI() may not exist when a restore starts. This
vignette explains how shinysnap waits for them and how to read the
report that each restore produces.
Shiny’s update*Input() functions send messages through
session$sendInputMessage(). In the browser, Shiny looks for
an input with the given id that it has bound, or connected to
the server. If there is no such input, Shiny drops the message without
logging anything.
This causes problems when you restore inputs created by
renderUI(). Any message sent before the input is ready is
lost. You can try adding delays, but a delay that works on one machine
may be too short on another. Nested dynamic UI makes this harder because
each set of inputs must wait for the previous set.
The server and shinysnap’s browser script work together during a restore:
reactiveValues, runs the snap_on_restore()
hooks, and sends all input values to the browser in one
message. Each value is converted to the format its input binding
expects.receiveMessage(), and keeps the
others pending.restoreInput() function. Shiny’s input constructors
call this function, so new inputs can start with their saved values
already in the HTML. This avoids briefly showing defaults and triggering
observers with those defaults. The browser reports these inputs as
constructed without applying their values again.The order of events matters here. Shiny sends a new input’s initial
value to the server just after the shiny:bound event.
Applying a saved value inside that event handler would let the initial
value overwrite it. shinysnap waits until that step has finished before
applying the saved value.
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 was recreated during the restore and received the value
again, without help from restoreInput() |
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 record the restore id,
duration, and whether it finished after a quiet period or a timeout.
missing and failed inputs produce a single
warning by default; snap_restore(unknown = "skip") silences
it and unknown = "error" rejects the promise instead.
A missing row can mean the input was removed in a newer
version of the app, or that the saved state does not display its UI. Two
common cases are:
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.The restore continues after snap_restore() returns. Its
return value is a handle: a list containing the restore id and
a promise for the report. You can receive the report through a callback,
a hook, or the promise:
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
})If an observer returns a promise, Shiny waits for it to resolve before sending updates to the browser. But the restore needs those updates to finish and produce its report. Returning the restore’s promise from an observer would therefore leave both waiting indefinitely.
Returning the handle is safe because it is an ordinary list. If you
use promises::then() inside an observer, end the observer
with NULL as above so it does not return a promise.
snap_is_restoring() is TRUE from the moment
snap_restore() is called until the report arrives. Use it
to skip expensive calculations while inputs are still changing, then run
them when the restore finishes:
snap_restore() during a restore cancels the
first restore, whose promise rejects with a condition of class
shinysnap_cancelled.snap_restore(use_restore_context = FALSE) when
debugging to disable the use of restoreInput(). Dynamic
inputs still receive their saved values, but are reported as
applied or reapplied instead of
constructed. Their observers see the default value before
the saved one.