install.packages("shinystate")
I am extremely thrilled to announce that my new package {shinystate}
is now available on CRAN! {shinystate}
enables powerful customization of the built-in Shiny bookmarkable state feature. This package has been over seven years in the making, and the journey to this milestone has been just as important as the destination as I’ll share in the conclusion of this post.
You can install {shinystate}
from CRAN with:
This blog post will cover my motivation for creating the package, the workflow to enable {shinystate}
in your Shiny application, features that supercharge the existing bookmarkable state capability, and a glimpse of key items I hope to solve before the next release.
Why shinystate
?
In 2016, version 0.14 of Shiny introducted a new feature called bookmarkable state, enabling Shiny applications to save and restore the state of application input values and optionally reactive values. I was an early adopter of this feature, and it was very helpful for my applications with a relatively small number of inputs and outputs. As my production-grade applications grew in complexity, I encountered a few limitations:
- The URL method of recording application state (where the input parameters are encoded inside the application URL address) would surpass the length of a URL allowed in a web browser’s address bar.
- When moving to the server method of recording application state (where the state of the application is saved to disk via custom
rds
objects), these state files where saved to a location within the hosting server’s installation of Shiny Server (later Posit Connect) that only an administrator of that platform could access. - The default implementation lacked an easy way to track multiple bookmarkable states saved throughout the lifecycle of using a complex application.
At the inagural R/Pharma conference held in 2018, the creator of Shiny Joe Cheng gave an enlightening keynote presentation Using Interactivity Responsibly in Pharma. Joe created an example Shiny application to illustrate novel approaches, including slight customizations to the default bookmarkable state feature which allowed the user to save multiple sessions and choose which sessions to restore. You can see the application source code at the GitHub repository. This was a launching point leading me to create a collection of functions and callback overrides that I utilized across multiple applications to solve the aforementioned issues. After copying these functions (and making small modifications each time) from one app to another, it became quite obvious that this situation called for a new package {shinystate}
!
Getting Started
Using {shinystate}
in a Shiny application requires a few small additions to your Shiny application. Below is a relatively small application demonstrating the steps above. You can view annotations for the relevant code by hovering your mouse over the annotation markers on the right margin. Thanks to the magic of {shinylive}
, you can try this application on this very blog post!
Show app code
library(shiny)
library(bslib)
library(shinystate)
storage <- StorageClass$new()
ui <- function(request) {
page_sidebar(
title = "Basic App",
sidebar = sidebar(
accordion(
open = c("user_inputs", "state"),
accordion_panel(
id = "user_inputs",
"User Inputs",
textInput(
"txt",
label = "Enter Title",
placeholder = "change this"
),
checkboxInput("caps", "Capitalize"),
sliderInput(
"bins",
label = "Number of bins",
min = 1,
max = 50,
value = 30
)
),
accordion_panel(
id = "state",
"Bookmark State",
actionButton("bookmark", "Bookmark"),
actionButton("restore", "Restore Last Bookmark")
)
)
),
use_shinystate(),
card(
card_header("App Output"),
plotOutput("distPlot")
)
)
}
server <- function(input, output, session) {
storage$register_metadata()
plot_title <- reactive({
if (!shiny::isTruthy(input$txt)) {
value <- "Default Title"
} else {
value <- input$txt
}
if (input$caps) {
value <- toupper(value)
}
return(value)
})
output$distPlot <- renderPlot({
req(plot_title())
x <- faithful$waiting
bins <- seq(min(x), max(x), length.out = input$bins + 1)
hist(
x,
breaks = bins,
col = "#007bc2",
border = "white",
xlab = "Waiting time to next eruption (in mins)",
main = plot_title()
)
})
observeEvent(input$bookmark, {
storage$snapshot()
showNotification("Session successfully saved")
})
observeEvent(input$restore, {
session_df <- storage$get_sessions()
storage$restore(tail(session_df$url, n = 1))
})
setBookmarkExclude(c("bookmark", "restore"))
}
shinyApp(ui, server, enableBookmarking = "server")
- 1
-
Create an instance of the
StorageClass
class outside of the application user interface and server functions - 2
-
Include
use_shinystate()
in your UI definition - 3
-
Call the
register_metadata()
method from your instance of theStorageClass
class at the beginning of the application server function - 4
-
Call the
snapshot()
method from your instance of theStorageClass
class to save the state of the Shiny app session - 5
-
Call the
restore()
method from your instance of theStorageClass
class to restore a saved session based on the session URL, available in the data frame returned from theget_sessions()
method. - 6
-
Enable the save-to-server bookmarking method by adding
enableBookmarking = 'server'
in the call toshinyApp()
Power-Up
The sample application above acts much like the default bookmarkable state method of only restoring the most recent saved session. But the value of {shinystate}
really comes to light when you use the following key features:
- Obtain all of the previously-saved state sessions as a tidy data frame with the
get_sessions()
method of yourStorageClass
instance, and potentially letting the user select from any of their previously-saved sessions. - Attaching optional metadata to be saved along with the actual bookmarkable state session objects to the session data frame, accomplished by sending a list of values via the
metadata
parameter in thesnapshot()
method, a convenient way to augment human-readable information alongside the rather cryptic unique ID associated with each saved session. - Building upon the immensly-underrated
{pins}
package, you can choose to save the bookmarkable state sessions to any location (referred to as boards) that{pins}
supports, including a Posit Connect server, S3-compatible object storage bucket in cloud providers like Amazon Web Services, Google Drive, and much more.
Blending these capabilities together unlocks great capabilities that have proven to be critical in production use of Shiny. From my personal experience creating production-grade Shiny applications used in critical decision making, it was very important to enable my users to choose from multiple sessions and be able to pull up a given saved session on the spot, often in these highly-important discussions with stakeholders. You can see a few of these ideas in action by reading the bookmark module vignette from the {shinystate}
documentation site.
Future Plans
The foundation has been set with this initial release, but I already have a handful of key tasks to pursue for the next version:
- Compatibility with application framework packages
{golem}
and{rhino}
. It very well could be the case that{shinystate}
already has the necessary underpinnings, but I will pursue creating example applications using these packages to find out. - Ensure
{shinystate}
can save and restore bookmarkable state wtih applications that employ dynamic UI generation. This has been a thorny issue in my previous applications, and I had to string together manual hacks to add a boatload of parameters to various UI modules to trick the application during the restore process. After multiple conversations and conference presentations such as Sharing app state between modules by Marcin Dubel, I have been exploring the use of{R6}
objects to manage the state of dynamic UI applications as seen in my dynamicui_state GitHub repository, and I have a small application demonstrating the saving and restoring of{R6}
objects available in the package. Merging these ideas together could be a powerful pattern for an optimal user experience. - A powerful framework for interactive exploration of data in Shiny that is gaining a lot of momentum in life sciences is
{teal}
, part of the Pharmaverse suite of packages. One of the many great features of{teal}
is the ability to create reproducible reports with the standalone R code to generate the same results as the application. The user can add one or more “cards” to a given report. There is a great opportunity for{shinystate}
to assist with the saving and restoring of these components.
I can’t wait to see how {shinystate}
unlocks new possibilities for managing application state in your next Shiny application! This is a brand new package, and do not hesistate to submit a new issue on the package’s GitHub repository if you encounter any problems or have a new feature request.
Behind the Code
A key motivation for launching the R-Podcast, Shiny Developer Series, and R Weekly Highlights was to share the brilliant efforts by the community of R developers and users, both on the technical side as well as the human side of these amazing people. Rest assured, nothing will ever disuade me from continuing these efforts. There’s always been a part of me that felt like I was very much a “consumer” of these tools, and early in my career I very much felt like what fellow podcaster/software engineer Michael Dominick coined as a dark-matter developer, meaning that my biggests technical achievements in statistical software development would never see the light of day outside the day job’s firewall. It didn’t always seem that way in the beginning, as I was lucky enough to co-author an R package performing statistical multiplicity adjustments called {multxpert}
back in 2011. I could write an entire blog post about my journey since that point, but suffice to say I’ve been eager for an opportunity to make even a small technical contribution again. I’ve benefited so much from Shiny on many levels, and it feels really cool to finally contribute a small bit to the Shiny ecosystem.