---
title: "Snapshots and bookmarks"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Snapshots and bookmarks}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

Shiny's bookmarking (`enableBookmarking()`) and shinysnap solve
neighbouring problems, and an app can use both. This vignette compares
them, shows how they coexist, and lists what to change when moving an app's
save-and-restore feature from one to the other.

## What each one does

| | bookmarking | shinysnap |
|---|---|---|
| state lives in | a URL (`"url"`) or a server directory (`"server"`) | a file the user downloads and uploads |
| restore happens | by loading the URL: a new session, a page reload | into the running session, no reload |
| dynamic UI | restored at construction through `restoreInput()` | restored at construction *and* by the client as inputs appear |
| requires | a UI function, `enableBookmarking()` | nothing in the UI |
| readable by people | no (URL-encoded JSON, or `.rds` files) | yes (JSON) |
| survives app changes | no built-in help | app name and version in the file, `validate` and `migrate` hooks, a report of what did not apply |

Bookmarking is the right tool for "send a colleague a link to what I am
looking at". shinysnap is the right tool for "save my work to a file, come
back next month, possibly on a newer version of the app".

## Coexistence

shinysnap reuses the parts of bookmarking's machinery that make sense
mid-session, so an app that already bookmarks keeps working:

- Ids excluded with `setBookmarkExclude()` are excluded from snapshots too.
- Values that shiny's serializers mark as unserializable (passwords, and
  anything registered with `setSerializer()`) are never captured.
- During a restore, shinysnap primes the session's restore context, the
  object behind `restoreInput()`, with the snapshot's values, and puts the
  previous context back when the restore settles.
- shinysnap's own internal inputs are marked unserializable, so they never
  show up in a bookmark URL.

The hooks mirror each other. `onBookmark(function(state) ...)` writes into
`state$values`; so does `snap_on_save()`. `onRestore()` reads
`state$values`; `snap_on_restore()` reads the file's `values`, and
`snap_track()` writes them back into your `reactiveValues` for you.

## Turning a snapshot into a bookmark

`snap_as_bookmark_url()` encodes a snapshot the way URL bookmarking does,
so that a saved state can also be opened as a link, provided the app has
`enableBookmarking("url")` and a UI function:

```{r}
library(shinysnap)
snap <- list(
  inputs = list(n = 100L, model = "complex", weights = c(0.5, 0.75)),
  values = list(note = "baseline")
)
snap_as_bookmark_url(snap, base_url = "https://example.org/app/")
```

Inside a server function, pass `session` instead of `base_url` and the
protocol, host, port, and path the browser used are filled in. The query
string carries the same `_inputs_` and `_values_` keys, with the same
encoding, as the URL `session$doBookmark()` produces for the same state.

## Migrating an app

1. Replace `enableBookmarking()` and the `bookmarkButton()` with
   `snap_download_button()` and `snap_file_input()` in the UI, and
   `snap_download_handler()` and `snap_file_restore()` in the server. The
   UI no longer needs to be a function.
2. Replace `onBookmark()` hooks with `snap_on_save()`, and `onRestore()`
   hooks with `snap_track()` for `reactiveValues` (they are written back for
   you) or `snap_on_restore()` for anything else.
3. Keep `setBookmarkExclude()` calls; add `snap_enable(exclude = ...)`
   patterns for ids that should never be saved.
4. Give the app a name and a version with `snap_enable()`, and write a
   `migrate` hook the first time a saved value changes meaning.
5. For tests, `snap_as_test_inputs()` turns a saved file into the
   `session$setInputs()` call of a `shiny::testServer()` test.
