| Title: | Reproducible Code for 'Shiny' Objects |
|---|---|
| Description: | Provides functionality to extract reactive expressions from a 'shiny' application and convert them into stand-alone R scripts. This enables users to reproduce tables and visualisations outside the interactive UI, facilitating integration into static reports or automated workflows without requiring access to the original application source code. |
| Authors: | Ashley Baldry [aut, cre] |
| Maintainer: | Ashley Baldry <ashley.baldry@acuityanalytics.com> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-09-20 12:21:42 UTC |
| Source: | https://github.com/AscentSoftware/shinyreprex |
Record reactives against the current Shiny session, so that
reprex_packages() and reprex_lockfile() can be called with no arguments
and still cover the whole application.
This avoids having to return reactives out of every module purely so a single top-level call can see them. Each module registers what it owns, and the session holds the collection.
Registrations are namespaced by the calling module, so two modules may register reactives of the same name without collision, and re-registering the same name in the same module replaces the previous entry rather than adding a duplicate.
Registering does not evaluate the reactive. Packages are resolved by reading
the expression held in the reactive, so it may be registered while still
gated behind shiny::req() or inputs that have yet to be set. Registering at
module setup is therefore both safe and preferred, as it does not depend on
the user having visited the output first.
The collection lives on the session, so it is discarded when the session ends and is never shared between concurrent users of the same application.
register_reactives(..., session = shiny::getDefaultReactiveDomain())register_reactives(..., session = shiny::getDefaultReactiveDomain())
... |
One or more |
session |
The Shiny session to register against. Defaults to the current reactive domain, which inside a module is that module's session. |
The registered reactives, invisibly, as a named list.
reprex_packages() and reprex_lockfile(), which read the
registered collection when called with no reactives.
library(shiny) summaryServer <- function(id) { moduleServer(id, function(input, output, session) { summary_tbl <- reactive(purrr::keep(iris, is.numeric)) register_reactives(summary_tbl) summary_tbl }) } # Elsewhere in the application, with no reactives threaded through: # reprex_lockfile()library(shiny) summaryServer <- function(id) { moduleServer(id, function(input, output, session) { summary_tbl <- reactive(purrr::keep(iris, is.numeric)) register_reactives(summary_tbl) summary_tbl }) } # Elsewhere in the application, with no reactives threaded through: # reprex_lockfile()
Capture the exact package versions, sources and R version needed to reproduce
one or more shiny::reactive objects, as an renv lockfile. Restoring the
lockfile with renv::restore() recreates the environment the reactives were
generated in, including the recursive dependency tree.
A single lockfile covers every reactive passed, so an application can offer one download that reproduces all of its outputs.
The snapshot is taken from the currently loaded library (.libPaths()), so
the versions recorded are exactly those the running Shiny session used to
produce the reactives. renv resolves and records the full recursive
dependency tree of packages, so only the top-level set needs to be supplied.
The snapshot runs against a throwaway temporary project, so it never writes
renv infrastructure into, or otherwise modifies, the application's own
directory.
A relative lockfile is resolved against the working directory, not against
the temporary project used for the snapshot. The default therefore writes
renv.lock into whichever directory the application is running from. Pass an
absolute path to control where it lands; inside a
shiny::downloadHandler() that is the file argument.
reprex_lockfile( ..., packages = NULL, lockfile = "renv.lock", exclude = NULL, session = shiny::getDefaultReactiveDomain() )reprex_lockfile( ..., packages = NULL, lockfile = "renv.lock", exclude = NULL, session = shiny::getDefaultReactiveDomain() )
... |
One or more |
packages |
Character vector of package names to snapshot. If |
lockfile |
Path to write the lockfile to. Defaults to |
exclude |
Character vector of package names to omit from the snapshot. |
session |
The Shiny session to read registered reactives from. Only used
when |
The absolute path the lockfile was written to, invisibly.
reprex_packages() to inspect the detected set without writing a
lockfile, and register_reactives() to record reactives from within each module.
library(shiny) numeric_iris <- reactive(purrr::keep(iris, is.numeric)) isolate(reprex_lockfile(numeric_iris, lockfile = tempfile(fileext = ".lock")))library(shiny) numeric_iris <- reactive(purrr::keep(iris, is.numeric)) isolate(reprex_lockfile(numeric_iris, lockfile = tempfile(fileext = ".lock")))
Extract the set of non-base packages needed to reproduce one or more
shiny::reactive objects, with duplicates removed across every reactive
passed.
Use this when building a custom UI around reprex_lockfile(): it gives you
the list of detected packages to present to the user for selection.
Packages are found by reading the expression held in each reactive rather
than by generating its script, so every branch of an if or switch
contributes. The result may therefore be a superset of the library() calls
reprex_reactive() emits for the branch actually taken, on the basis that a
lockfile is safer holding a package that is not needed than missing one.
reprex_packages(..., session = shiny::getDefaultReactiveDomain())reprex_packages(..., session = shiny::getDefaultReactiveDomain())
... |
One or more |
session |
The Shiny session to read registered reactives from. Only used
when |
A character vector of unique package names. Base packages are excluded, as they require no installation.
reprex_lockfile() to turn this set into an renv lockfile, and
register_reactives() to record reactives from within each module.
library(shiny) numeric_iris <- reactive(purrr::keep(iris, is.numeric)) styled_code <- reactive(styler::style_text("1 + 1")) # Outside a running application, isolate supplies the reactive context isolate(reprex_packages(numeric_iris, styled_code))library(shiny) numeric_iris <- reactive(purrr::keep(iris, is.numeric)) styled_code <- reactive(styler::style_text("1 + 1")) # Outside a running application, isolate supplies the reactive context isolate(reprex_packages(numeric_iris, styled_code))
Construct the code within a given shiny::reactive object
to be able to re-create the output outside of a Shiny session.
reprex_reactive(x)reprex_reactive(x)
x |
|
A character string, that when printed (using base::cat),
displays the script that reproduces the contents of x.
library(shiny) ui <- fluidPage( h1("Reproducible Code Example"), inputPanel( sliderInput( "min_width", "Minimum Petal Width", min(iris$Petal.Width), max(iris$Petal.Width), min(iris$Petal.Width), step = 0.1 ), selectInput( "summary_fn", "Summary Function", c("Mean" = "mean", "Median" = "median", "SD" = "sd"), selected = "mean" ) ), fluidRow( column( width = 5, h2("Table"), tableOutput("table") ), column( width = 7, h2("Code"), verbatimTextOutput("code") ) ) ) server <- function(input, output, session) { iris_filt <- reactive({ iris[with(iris, Petal.Width > input$min_width), ] }) summary_tbl <- reactive({ aggregate( Sepal.Width ~ Species, data = iris_filt(), FUN = get(input$summary_fn) ) }) output$table <- renderTable(summary_tbl()) output$code <- renderText(reprex_reactive(summary_tbl)) } if (interactive()) { shinyApp(ui, server) }library(shiny) ui <- fluidPage( h1("Reproducible Code Example"), inputPanel( sliderInput( "min_width", "Minimum Petal Width", min(iris$Petal.Width), max(iris$Petal.Width), min(iris$Petal.Width), step = 0.1 ), selectInput( "summary_fn", "Summary Function", c("Mean" = "mean", "Median" = "median", "SD" = "sd"), selected = "mean" ) ), fluidRow( column( width = 5, h2("Table"), tableOutput("table") ), column( width = 7, h2("Code"), verbatimTextOutput("code") ) ) ) server <- function(input, output, session) { iris_filt <- reactive({ iris[with(iris, Petal.Width > input$min_width), ] }) summary_tbl <- reactive({ aggregate( Sepal.Width ~ Species, data = iris_filt(), FUN = get(input$summary_fn) ) }) output$table <- renderTable(summary_tbl()) output$code <- renderText(reprex_reactive(summary_tbl)) } if (interactive()) { shinyApp(ui, server) }