reprex_reactive takes a reactive object and converts it
into a script that can be reused outside of the Shiny application to
reproduce the result of the reactive. This can be sent to a simple
verbatimTextOutput or something more UX friendly such as
the {highlighter} package to display the script in the
UI.
A script on its own reproduces the code, but not the environment it ran in. The remaining three functions close that gap:
| Function | Purpose |
|---|---|
reprex_reactive |
Turn a reactive into a stand-alone script |
reprex_packages |
List the packages needed to run that script |
reprex_lockfile |
Capture those packages, their versions and sources as an
renv lockfile |
register_reactives |
Record reactives so the two functions above can be called with no arguments |
A worked example covering all four ships with the package:
reprex_reactive emits the library() calls a
script needs, but not the versions those packages were at.
reprex_lockfile records them, along with the R version and
the full recursive dependency tree, so the environment can be rebuilt
rather than approximated.
output$lockfile <- downloadHandler(
filename = function() "renv.lock",
content = function(file) reprex_lockfile(summary_tbl, lockfile = file)
)Whoever receives the lockfile restores it with renv:
Use reprex_packages when you want to show the detected
packages before pinning them, for example to let the user narrow the
set:
Supplying packages is also the escape hatch when the
detector cannot see a package, such as one attached only for an operator
or an S3 method.
In a modular application the reactives worth reproducing live inside their own modules. Rather than returning them all up to the top level, each module registers what it owns:
summaryServer <- function(id) {
moduleServer(id, function(input, output, session) {
summary_tbl <- reactive({
aggregate(Sepal.Width ~ Species, data = iris, FUN = get(input$summary_fn))
})
register_reactives(summary_tbl)
summary_tbl
})
}reprex_lockfile() and reprex_packages()
then cover the whole application when called with no arguments.
Registrations are namespaced by module, held on the session, and
discarded when the session ends, so they are never shared between
concurrent users.
reprex_reactive call to EventThe code to reproduce a given reactive will be updated whenever an input or reactive feeding into the provided reactive is updated, therefore it is recommended to have the reactive as an event attached.
register_reactives does not evaluate the reactive it is
given, so it can be called as soon as the reactive is defined, even when
that reactive is still gated behind shiny::req or inputs
that have yet to be set. Registering at setup means a lockfile covers
every module regardless of which parts of the application the user
happened to visit.
# Good
summary_tbl <- reactive({
shiny::req(input$summary_fn)
aggregate(Sepal.Width ~ Species, data = iris, FUN = get(input$summary_fn))
})
register_reactives(summary_tbl)
# Bad - the reactive is only registered once the user has viewed the output,
# so a lockfile downloaded beforehand silently misses it
output$table <- renderTable({
register_reactives(summary_tbl)
summary_tbl()
})This is general best-practice when developing Shiny applications, but
avoid putting code for its side effects in reactive
expressions, and instead create smaller reactive calls, and have
observers running the code not intended for reproducing outputs.
# Good
width_range <- reactive({
iris_filt <- dplyr::filter(iris, Species == "versicolor")
range(iris_filt$Petal.Width)
})
observe({
updateSliderInput("width", width_range())
})
# Bad
width_range <- reactive({
iris_filt <- dplyr::filter(iris, Species == "versicolor")
widths <- range(iris_filt$Petal.Width)
updateSliderInput("width", widths)
widths
})
summary_data <- reactive({
iris_filt <- dplyr::filter(iris, Species == "versicolor")
shinyjs::toggle("table", condition = nrow(iris_filt) > 0L)
dplyr::summarise(iris_filt, dplyr::across(where(is.numeric), mean))
})In order that developers can easily recreate outputs generated in Shiny applications, add any business logic, such as ETL, data manipulation and modelling, to a separate package. This will allow users to recreate the tables and plots generated in the app without having to install all the packages associated with the application.
If you are using secrets, such as environment variables, make sure that they are defined within a reactive expression. If it is defined in the module, or in the global environment, the secret will be written in the assignment.
Reproducible code is generated by reading the expression held in a reactive, rather than by tracing what it does when it runs. That keeps the process cheap and independent of the current inputs, but it sets some boundaries worth knowing about.
Calls made inside a function that is defined elsewhere are not visited, so anything used only in that function is neither reproduced nor detected as a package.
summarise_widths <- function(dat) dplyr::summarise(dat, mean(Petal.Width))
my_reactive <- reactive(summarise_widths(iris))Here the script reproduces the call to summarise_widths,
but dplyr is not reported. Keeping the work inside the
reactive, or moving it into a package that the recipient installs,
avoids this. See also Create a Business Logic Package
above.
Data-masking functions such as dplyr::filter or
subset refer to columns as if they were variables. Since
the column names are only distinguishable from real variables at run
time, a collision between the two can produce a misleading line in the
script:
moduleServer(id, function(input, output, session) {
Species <- "versicolor"
my_reactive <- reactive(subset(iris, Species == "setosa"))
})Species in the reactive is a column of
iris, but a variable of the same name exists in the module,
so the script includes an unnecessary
Species <- "versicolor" assignment. The reproduced code
still runs, because subset masks the variable with the
column, but the extra line is misleading. Avoiding names that clash with
the columns being masked avoids this.
Similarly, an input captured with rlang::quo and spliced
back in later cannot be resolved to its value, and appears in the script
with the !! still in place.
reprex_packages walks every branch of an if
or switch rather than evaluating the condition and
following only the branch that would be taken. A lockfile may therefore
pin a package that the script does not end up needing. This is
deliberate: a lockfile holding a package that is not needed is safer
than one missing a package that is.