Shiny has bookmarking. It captures a state that you can send as a
link. Shiny also has reactlog. It lets you replay
a session while you debug. But Shiny has no undo. Users expect undo,
because each other application has it. rewind adds undo in
one line.
Install
Install rewind from CRAN:
install.packages("rewind")To get the development version from GitHub:
# install.packages("remotes")
remotes::install_github("tenmeh/rewind")The one-line version
library(shiny)
library(rewind)
ui <- fluidPage(
rewind_buttons(),
selectInput("region", "Region", c("North", "South", "East", "West")),
sliderInput("year", "Year", 2018, 2026, 2024),
plotOutput("plot")
)
server <- function(input, output, session) {
rewind_enable() # <- that's it
output$plot <- renderPlot(plot_for(input$region, input$year))
}
shinyApp(ui, server)Ctrl+Z now moves backwards through the filter choices of
the user. Ctrl+Shift+Z moves forwards. rewind
captures every input in the session automatically. There is a short list
of exclusions: action buttons, fileInput(), and the inputs
of this package. ?rewind_enable gives the full list.
Keyboard shortcuts
| Keys | Action |
|---|---|
Ctrl + Z (Cmd +
Z on macOS) |
Undo |
Ctrl + Shift + Z
(Cmd + Shift + Z on macOS) |
Redo |
Ctrl + Y
|
Redo |
There are two shortcuts for redo. Ctrl + Y
is the usual one on Windows. Cmd + Shift +
Z is the usual one on macOS.
The shortcuts do nothing while the user types in a text field. The text undo of the browser thus continues to work:
rewind_enable(shortcuts = FALSE) # turn the shortcuts offOne movement of a slider is one undo step, not forty
sliderInput sends an input change for each pixel of a
movement. Without grouping, you would press Ctrl+Z forty
times to return to the start. rewind puts the changes that
occur within coalesce_ms of each other into one history
entry. The default is 400 ms.
rewind_enable(coalesce_ms = 600) # make larger groupsIncrease coalesce_ms for widgets that send changes very
quickly. Decrease it for smaller undo steps.
Grouping several changes into one step
A “reset filters” button changes four inputs together. It must make
one undo entry, with a label that a person wrote. It must not make four
entries, and it must not use an automatic label such as “4 values
changed”. Put the block inside rewind_step():
observeEvent(input$reset, {
rewind_step(label = "Reset filters", {
updateSelectInput(session, "region", selected = "All")
updateSliderInput(session, "year", value = c(2018, 2026))
updateCheckboxInput(session, "active_only", value = FALSE)
})
})Undo for state that is not an input
rewind cannot see the values that you keep in
reactiveValues(). These can be a list of pinned items, a
current selection, or a cache that you want to undo. Register them with
rewind_track():
server <- function(input, output, session) {
rewind_enable()
state <- reactiveValues(pinned = character(0))
rewind_track(state, fields = "pinned")
observeEvent(input$pin, {
state$pinned <- c(state$pinned, input$item)
})
}An undo now restores the pin list. It also restores the inputs that changed at the same time.
A visual history rail
rewind_ui() draws the stack as a scrubbable list. Click
a step to move to it. The labels come from the values that changed. The
rail thus shows “region, year” and not “state 7”:
ui <- fluidPage(
sidebarLayout(
sidebarPanel(rewind_ui()),
mainPanel(plotOutput("plot"))
)
)Modules
You can call rewind_enable() inside a
moduleServer(). rewind captures the inputs
with their module-local names. It adds the namespace with
session$ns() at a restore. This is the same as each other
operation on Shiny inputs inside a module.
All modules share session$userData. Call
rewind_enable() once, at the position in the module tree
that is best for your application. A second call in a different module
uses the same history. It does not make a second history.
Stop capture around changes that your code makes
Your code can restore a saved session, or apply a URL bookmark at the
start. These changes must not become an undo step. Put that code between
rewind_pause() and rewind_resume():
server <- function(input, output, session) {
rewind_enable()
rewind_pause()
# ... apply a saved bookmark here ...
rewind_resume()
}Use rewind_disable() to stop undo and redo completely
during a session. An example is a user role that the application knows
only after the session starts.
What to read next
-
?rewind_enablefor the full list of the inputs thatrewindcaptures - The README for the mechanics of how restore works and the echo problem it has to avoid
- The reference index for every exported function