--- title: "Introduction to shinyreprex" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to shinyreprex} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(shinyreprex) ``` ## Using shinyreprex `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: ```r shiny::runExample("lockfile", package = "shinyreprex") ``` ## Reproducing the Environment `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. ```r output$lockfile <- downloadHandler( filename = function() "renv.lock", content = function(file) reprex_lockfile(summary_tbl, lockfile = file) ) ``` Whoever receives the lockfile restores it with `renv`: ```r renv::restore(lockfile = "renv.lock") ``` Use `reprex_packages` when you want to show the detected packages before pinning them, for example to let the user narrow the set: ```r reprex_lockfile(summary_tbl, packages = input$packages, lockfile = file) ``` 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. ### Registering Reactives Across Modules 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: ```r 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. ## Best Practices ### Bind `reprex_reactive` call to Event The 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. ```r width_range <- reactive({ iris_filt <- dplyr::filter(iris, Species == "versicolor") range(iris_filt$Petal.Width) }) # Good repro_range <- reactive(reprex_reactive(width_range)) |> bindEvent(width_range()) # Bad repro_range <- reactive(reprex_reactive(width_range)) ``` ### Register Reactives at Module Setup `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. ```r # 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() }) ``` ### Put Side-Effects in Observers 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. ```r # 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)) }) ``` ### Create a Business Logic Package 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. ### Use Secrets in Reactives 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. ```r # Good moduleServer(id, function(input, output, session) { my_reactive <- reactive({ api_key <- Sys.getenv("MY_API_KEY") ... }) }) # Bad moduleServer(id, function(input, output, session) { api_key <- Sys.getenv("MY_API_KEY") my_reactive <- reactive({ ... }) }) ``` ## Limitations 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. ### Only the Reactive Expression is Read 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. ```r 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. ### Non-Standard Evaluation is Best-Effort 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: ```r 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. ### Detected Packages Can Be a Superset `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.