shinysnap lets users save their
work in a Shiny app and return to it later. A snapshot contains
the app’s input values and any values you choose to keep from the
server. Users can download it as a JSON file, share it, and upload it to
restore their work in another session. The app stays open throughout the
restore. You don’t need shiny::enableBookmarking().
Here, a snapshot means a copy of the app’s state. Snapshot tests in testthat and shinytest2 serve a different purpose: they record output to check for unexpected changes. shinysnap does not take screenshots.
Add a download button and a file input to the UI, then connect them to handlers in the server function:
library(shiny)
library(shinysnap)
ui <- fluidPage(
sliderInput("n", "Sample size", 10, 500, 100),
selectInput("model", "Model", c("simple", "complex")),
uiOutput("model_inputs"),
snap_download_button("save"),
snap_file_input("restore")
)
server <- function(input, output, session) {
snap_enable(app = "demo", version = "1.0.0")
output$model_inputs <- renderUI({
if (input$model == "simple") {
numericInput("rate", "Rate", 0.05)
} else {
sliderInput("k", "k", 1, 10, 3)
}
})
snap_download_handler("save")
snap_file_restore("restore")
}
shinyApp(ui, server)Click Save state to download a .json
file with the current input values. Upload it later to restore those
values in the same app. For example, a snapshot with
model = "complex" selects that model, which creates the
k slider. The slider then receives its saved value. You
don’t need to add delays to wait for it to appear.
You can read snapshot files in a text editor. Numbers use the fewest
digits needed to preserve their value. Values that JSON cannot represent
directly, such as dates, matrices, NA, and empty vectors,
include extra fields that record their type. Here’s an example:
library(shinysnap)
snap <- list(
app = list(name = "demo", version = "1.0.0"),
created = "2026-09-16T18:22:03Z",
inputs = list(
dates = as.Date(c("2024-01-01", "2024-03-01")),
model = "complex",
n = 100L,
rate = 0.025,
weights = c(0.5, 0.75)
),
values = list(prefs = list(digits = 3L, scientific = FALSE))
)
cat(snap_serialize(snap))
#> {
#> "format": 1,
#> "app": {"name": "demo", "version": "1.0.0"},
#> "created": "2026-09-16T18:22:03Z",
#> "producer": {"shinysnap": "0.1.0", "shiny": "1.14.0", "r": "4.6.1"},
#> "inputs": {
#> "dates": {"$type": "Date", "value": ["2024-01-01", "2024-03-01"]},
#> "model": "complex",
#> "n": 100,
#> "rate": 0.025,
#> "weights": [0.5, 0.75]
#> },
#> "values": {"prefs": {"digits": 3, "scientific": false}},
#> "bindings": {},
#> "meta": {}
#> }Reading the file gives you the same R values. You can also edit the file in a text editor before restoring it:
snap_write().reactiveValues you
register with snap_track(), plus whatever your
snap_on_save() hooks add.If you already save your app’s state with
reactiveValuesToList(input) and an .rds file,
your restore code may look something like this. It sends each saved
value back to its input, handles matrices separately, and provides
defaults for fields added since the file was saved. It also adds delays
to wait for inputs created by renderUI().
# Before: restoring values manually (some code omitted)
observeEvent(input$restore_file, {
saved <- readRDS(input$restore_file$datapath)
is_matrix <- vapply(saved$inputs, is.matrix, logical(1))
for (id in names(saved$inputs)[!is_matrix]) {
session$sendInputMessage(id, list(value = saved$inputs[[id]]))
}
for (id in names(saved$inputs)[is_matrix]) {
updateMatrixInput(session, id, saved$inputs[[id]])
}
prefs$digits <- saved$prefs$digits
prefs$scientific <- if (is.null(saved$prefs$scientific)) FALSE else saved$prefs$scientific
# Inputs inside renderUI() do not exist yet: guess how long they take.
shinyjs::delay(500, {
for (id in c("detail_k", "detail_note")) {
if (!is.null(saved$inputs[[id]])) {
session$sendInputMessage(id, list(value = saved$inputs[[id]]))
}
}
})
shinyjs::delay(1500, {
# ... another wave for the inputs that appear after those ...
})
})With shinysnap, you register the values to save and provide functions to check files and update values from older versions:
# After
snap_enable(app = "myapp", version = "2.4.0", exclude = c("^btn_", "^nav$"))
snap_track(prefs)
snap_track(main_options)
snap_download_handler(c("btn_save_main", "btn_save_details"))
snap_file_restore(
c("btn_restore_main", "btn_restore_details"),
validate = function(snap) {
if (is.null(snap$inputs$model)) stop("This file was not saved by this app.")
},
migrate = function(snap, from) {
if (is.null(snap$values$prefs$scientific)) snap$values$prefs$scientific <- FALSE
snap
}
)shinysnap handles the remaining work:
vignette("dynamic-ui").vignette("custom-inputs").migrate function so you can supply
defaults for fields added since the file was saved. This keeps those
checks in one place.vignette("format-spec").vignette("dynamic-ui"): how a restore reaches inputs
inside dynamic UI, and how to read the restore report.vignette("custom-inputs"): restoring inputs from other
packages.vignette("format-spec"): the file format.vignette("migrating-from-bookmarks"): using shinysnap
with Shiny bookmarks.