Adds a user-facing undo and redo history to 'Shiny' applications. Application state, comprising registered inputs and optionally server-side reactive values, is captured as the user interacts with the application. Users may then step backwards and forwards through that history with the keyboard, with buttons, or by scrubbing a visual history rail. Rapid successive changes, such as dragging a slider, are coalesced into a single history entry, and related changes may be grouped explicitly into semantic steps.

Undo and redo for Shiny applications.
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.
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.
| 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. Set
rewind_enable(shortcuts = FALSE) to turn the shortcuts off.
install.packages("rewind")
To get the development version from GitHub:
# install.packages("remotes")
remotes::install_github("tenmeh/rewind")
Then run the demo:
shiny::runApp(system.file("examples/demo", package = "rewind"))
roxygen2 makes NAMESPACE and man/. Do not edit those files. Edit the
roxygen comments above each function. Then make the documents again:
devtools::document() # rewrites NAMESPACE and man/ from the roxygen comments
devtools::test() # runs tests/testthat
devtools::check() # full R CMD check
One movement of a slider sends many input events. An undo of each event is
of no use. Changes that occur within coalesce_ms (the default is 400) of
each other become one history entry. One movement is thus one undo step.
A "reset filters" button changes four inputs together. It must make one entry, with a name that a person wrote:
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)
})
})
rewind cannot see the values that you keep in reactiveValues. Register
them:
state <- reactiveValues(pinned = character(0), zoom = 1)
rewind_track(state, fields = c("pinned", "zoom"))
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(...)
)
)
Read this part if you intend to extend the package.
To restore a value, you must put it back in the widget. The simple method
is a lookup table: sliderInput -> updateSliderInput, selectInput ->
updateSelectInput, and one entry for each other input type. That table is
never complete. It fails when a person uses a widget from a package that
you do not know.
rewind thus has no table. Each Shiny input registers an input binding
on its DOM element. Each binding gives a setValue() or a
receiveMessage() function. The code in the browser finds the binding and
calls that function:
var binding = $el.data("shiny-input-binding");
if (typeof binding.setValue === "function") {
binding.setValue(el, value);
} else {
binding.receiveMessage(el, { value: value });
}
That is the full restore procedure. It also operates on inputs from other packages, with no extra code.
A restore sends the value to the browser. The browser sets the value. It then sends the new value back to the server. There, the capture observer sees a change and writes a new history entry. This is a loop. Packages of this type usually fail because of it.
Two methods stop the loop. They overlap on purpose:
Neither method assumes a time for the trip to the browser and back. The package is thus dependable, and not only usually correct.
fileInput(). Its value points to a temporary file on the server.
Shiny deletes that file at the next upload. An old snapshot would thus
point to a file that does not exist... These belong to Shiny.rewind_. These belong to this
package.These are the limits of the package. Read them before you start:
reactiveValues
and track it there. No serialisation occurs there.rewind_enable() inside a moduleServer().
This operation has tests. rewind captures the inputs with their
module-local names. These are the same names that input$ uses inside
the module. rewind adds the namespace with session$ns() at a restore.
All modules share session$userData. A call in a second module thus uses
the same history. Call the function once, at the position in the module
tree that is best for your application.renderUI makes. rewind captures them after they
exist. An undo to a state before they existed keeps their current values.| Function | Purpose |
|---|---|
rewind_enable() |
Start capture for the session |
rewind_track() |
Add reactiveValues fields to the history |
rewind_step() |
Put several changes into one entry with a label |
rewind_undo(), rewind_redo(), rewind_jump() |
Move through the history from your own controls |
rewind_clear() |
Remove each entry but the current one |
rewind_pause(), rewind_resume() |
Stop capture around changes that your code makes |
rewind_disable() |
Stop capture for the session completely |
rewind_history(), rewind_can_undo(), rewind_can_redo() |
Read the history in a reactive context |
rewind_buttons(), rewind_ui() |
Ready-made controls |
MIT