---
title: "Get started with shinysnap"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Get started with shinysnap}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

shinysnap lets users of a Shiny app save their work and pick it up later:
it takes a *snapshot* of the running app (the input values plus any
server-side values you register), writes it to a plain JSON file that can
be shared and kept under version control, and restores it into another
session without a page reload and without `shiny::enableBookmarking()`.

The word "snapshot" is used in the sense of a virtual machine or file system
snapshot: a saved state you can write to a file, share, and restore later.
It has nothing to do with snapshot *testing* (`expect_snapshot` in testthat
and shinytest2) or with screenshots.

## A minimal app

Two UI helpers and two server calls are all it takes:

```{r, eval = FALSE}
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)
```

Clicking **Save state** downloads a `.json` file with every input that is
currently on the page. Uploading that file later, in any session, puts
the app back into that state: the select is applied first, the dynamic UI
it controls re-renders, and the inputs inside it receive their values as
soon as they exist. You never write timing code.

## The file

Snapshot files are meant to be read by people. Numbers are written with
the fewest digits that read back to the same value, and everything JSON
cannot express directly (dates, matrices, `NA`, the type of an empty
vector) uses a small typed wrapper. This is what a file looks like:

```{r}
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))
```

Reading it back gives the same R values, so hand-editing a file and
restoring it is a supported workflow:

```{r}
text <- snap_serialize(snap)
back <- snap_unserialize(text)
str(snap_inputs(back))
```

## What gets saved

- **Inputs** that are on the page, by fully namespaced id. Values of inputs
  whose dynamic UI has been removed are dropped, so the file describes the
  UI as the user saw it. Action buttons, passwords, and file uploads are
  never stored as input values (uploads can travel in a zip bundle, see
  `snap_write()`).
- **Values** from the `reactiveValues` you register with `snap_track()`,
  plus whatever your `snap_on_save()` hooks add.
- The app name and version, so a restore can refuse files from another
  app and migrate files from an older version.

## The before and after

Apps that save state by hand usually end up with something like this:
`reactiveValuesToList(input)` into an `.rds` file, and on upload a loop of
`session$sendInputMessage()` calls, a special case for matrix inputs, and,
because inputs inside `renderUI()` do not exist yet when the first messages
are sent, staggered delays.

```{r, eval = FALSE}
# Before: a hand-rolled restore (abridged)
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, the same app needs no delays, no matrix special case, and
no per-field fallbacks:

```{r, eval = FALSE}
# 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
  }
)
```

What disappeared and why:

- The delays: values for inputs that are not on the page yet wait in the
  browser and are applied the moment the input is bound, and dynamic UI that
  re-renders during the restore is built with the restored values already
  in place (see `vignette("dynamic-ui")`).
- The matrix special case: the value of every input is turned into the
  message its binding understands by a *restorer*; shinysnap ships them
  for shiny, bslib, and shinyMatrix inputs (see `vignette("custom-inputs")`).
- The `is.null()` fallbacks: the `migrate` hook is the one place where
  defaults for values introduced after a file was saved belong.
- The `.rds` file: JSON is readable, diffable, and safe to open (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")`: shinysnap next to `enableBookmarking()`.
