Skip to contents

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.

Why dynamic inputs need special handling

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.

How shinysnap restores inputs

The server and shinysnap’s browser script work together during a restore:

  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 value is converted to the format its input binding expects.
  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 an input, the browser script applies the saved value for that id. For example, restoring a select input may create another group of inputs. Those inputs receive their values when they appear, even if they create further inputs in turn. An input that is recreated during the restore also receives its saved value again.
  4. During the restore, the server makes the saved values available to Shiny’s 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.
  5. The restore finishes once the page has been quiet for 0.3 seconds by default: Shiny is not busy and no UI or values are changing. It also has a timeout of 10 seconds by default. The browser then reports a status for every input.

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.

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:

#> <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:

  • 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

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.

Reacting during a restore

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:

observeEvent(input$n, {
  if (snap_is_restoring()) {
    return()
  }
  fit()
})

observe({
  if (!snap_is_restoring()) fit()
})

Notes on timing

  • Some inputs wait briefly before sending changes to the server. Text inputs wait for a pause in typing; sliders limit how often they send updates. A download handler takes its snapshot after the click reaches the server, which in practice gives these updates time to arrive. You don’t need to add delays.
  • Calling snap_restore() during a restore cancels the first restore, whose promise rejects with a condition of class shinysnap_cancelled.
  • Use 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.