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.
A minimal app
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.
The file
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:
text <- snap_serialize(snap)
back <- snap_unserialize(text)
str(snap_inputs(back))
#> List of 5
#> $ dates : Date[1:2], format: "2024-01-01" "2024-03-01"
#> $ model : chr "complex"
#> $ n : int 100
#> $ rate : num 0.025
#> $ weights: num [1:2] 0.5 0.75What gets saved
-
Inputs that are on the page, identified by their
full ids, including any module prefixes. Inputs whose UI has been
removed are left out. Action buttons, passwords, and file uploads are
never stored as input values. You can include uploaded files in a zip
bundle with
snap_write(). -
Values from the
reactiveValuesyou register withsnap_track(), plus whatever yoursnap_on_save()hooks add. - The app name and version, so shinysnap can reject files from another app and run your migration code for files from an older version.
Replacing your own restore code
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:
- It waits for dynamic inputs to appear before restoring their values.
Inputs created during a restore can also start with their saved values
already in place. See
vignette("dynamic-ui"). - It converts each saved value into the message its input expects. The
functions that do this are called restorers. shinysnap includes
them for shiny, bslib, and shinyMatrix. See
vignette("custom-inputs"). - It calls your
migratefunction so you can supply defaults for fields added since the file was saved. This keeps those checks in one place. - It writes JSON files that you can read, compare in version control,
and open safely. See
vignette("format-spec").
Learn more
-
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.
