Skip to contents

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:

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 off

One 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 groups

Increase 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”:

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.

  • ?rewind_enable for the full list of the inputs that rewind captures
  • The README for the mechanics of how restore works and the echo problem it has to avoid
  • The reference index for every exported function