8  Launch

Published

2024-09-13

Launching Apps:

  • shinyApp(): can be bundled in a standalone app function to store an app-package

    • Also useful for quick prototyping or if an app is in a single .R script

    • Returns a shiny app object (helpful with testing)

  • runApp(): versatile function that can launch apps in multiple formats/locations

    • A directory with a server.R and ui.R, or a single app.R file`

    • An app object created by shinyApp()

  • shinyAppDir(): useful if you need launch a Shiny app programmatically from a directory.


This chapter covers the differences between shinyApp(), runApp(), and shinyAppDir(), what should go in an app.R file, and other options for launching an app (or apps) from within an app-package.

At the time of this writing, the 2024.09.0-1 pre-release of Positron was available for testing.

During development, we have a variety of options for launching applications. Both RStudio and Positron offer a single-click button to run our application the top of the Source pane/Editor. We can also launch the app by calling the standalone app function in the Console. Below we’ll cover what happens behind the scenes when we click on these icons.

I’ve created the shinypak R package In an effort to make each section accessible and easy to follow:

Install shinypak using pak (or remotes):

# install.packages('pak')
pak::pak('mjfrigaard/shinypak')

Review the chapters in each section:

library(shinypak)
list_apps(regex = '^08')
## # A tibble: 1 × 2
##   branch        last_updated       
##   <chr>         <dttm>             
## 1 08_launch-app 2024-09-03 22:18:59

Launch an app:

launch(app = "08_launch-app")

8.1 ‘Running’ apps

In the current branch of sap, the app.R file contains a call to launch_app(), which loads the package and passes the UI and server functions to shinyApp().

See the 06.2_pkg-imports branch of sap.

As we can see in the image below, this places the Run App icon in RStudio’s Source pane. In Positron , the app.R file displays the Run Shiny App.

Clicking on either Run App/Run Shiny App buttons calls runApp() and launches our application, but with a loadSupport() warning.


The warning in the Console is telling us loadSupport() has detected that sap is a package, but it’s still sourcing the R/ subdirectory.1

The section below will cover the loadSupport() warning and why it appears when using the Run App/Run Shiny App buttons in RStudio and Positron .

8.1.1 loadSupport() warnings

‘Launch’ vs. ‘Run’

I’ll be using the term ‘launch’ to differentiate successfully displaying the application locally (either in the IDE window, viewer pane, or browser) from clicking on the Run App/Run Shiny App button or calling the runApp() function.

As noted above, clicking Run App in RStudio calls runApp() and launches our application with a loadSupport() warning printed in the Console.

Clicking Run App in RStudio calls runApp() and displays the loadSupport() warning

Clicking Run App in RStudio calls runApp() and displays the loadSupport() warning

In Positron , clicking on the Run Shiny App button launches the application with a slightly different process than RStudio :

Clicking Run Shiny App in Positron calls runApp() and displays the loadSupport() warning

Clicking Run Shiny App in Positron calls runApp() and displays the loadSupport() warning
  1. A new Shiny terminal process is started with the --devmode option
  2. runApp() is called with the full path to our app.R file and a port argument2
  3. The loadSupport() warning is displayed in the Terminal

It’s important to note that we also see the loadSupport() warning if we bypass the Run App/Run Shiny App buttons and call shiny::runApp() directly in the Console:

Calling runApp() directly in RStudio’s Console displays the loadSupport() warning

Calling runApp() directly in RStudio’s Console displays the loadSupport() warning

Calling runApp() directly in Positron’s Console displays the loadSupport() warning

Calling runApp() directly in Positron’s Console displays the loadSupport() warning

What’s with the warning?

In the Shiny chapter we learned loadSupport() is called automatically when an application is run with runApp(). Specifically, loadSupport()

loads any top-level supporting .R files in the R/ directory adjacent to the app.R/server.R/ui.R files3

Behind the scenes, runApp() is called by both Run App/Run Shiny App buttons, which then automatically calls loadSupport(). This is why if we pass our standalone app function to the Console (which uses shinyApp(), not runApp()), the warning disappears:

No loadSupport() warning in RStudio

No loadSupport() warning in RStudio

No loadSupport() warning in Positron

No loadSupport() warning in Positron

The examples above demonstrate the differences between calling shinyApp() and runApp(), and the tension between calling a standalone app function from an R package vs. sourcing the app.R file in a Shiny project.

8.1.2 --devmode

As we saw above, clicking Run Shiny App in Positron will launch the application in devmode,4 which is controlled by the script below:

show/hide Positron’s runShinyApp.R script
usage <- "Usage: Rscript runShinyApp.R <path> <port> [--devmode]"

args <- commandArgs(trailingOnly = TRUE)

if (length(args) < 2) {
  stop(usage)
}

path <- args[1]
port <- as.integer(args[2])
stopifnot(is.integer(port))
devmode <- "--devmode" %in% args

if (devmode) {
  shiny::devmode()
} else {
  options(shiny.autoreload = TRUE)
}

message("Running Shiny app")
message("-----------------")
message(sprintf('shiny::runApp(%s, port = %d)\n', deparse(path), port))

shiny::runApp(path, port = port, launch.browser = FALSE)
1
commandArgs(trailingOnly = TRUE) retrieves the arguments passed to the script, excluding the default arguments used when R starts.
2
devmode <- "--devmode" %in% args checks if the --devmode flag is present among the arguments. If it is, devmode is set to TRUE
3
Lastly, the script runs the Shiny app using runApp() (with the specified path and port), and launch.browser = FALSE ensures the app is not automatically opened in a web browser.

Shiny devmode will display something like the following messages the first time your application is launched using the Run Shiny App button in Positron :

show/hide devmode options
shiny devmode - Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`
This message is displayed once every 8 hours.
shiny devmode - Turning off caching of Sass -> CSS compilation. To turn caching on, call `options(sass.cache = TRUE)`
This message is displayed once every 8 hours.
shiny devmode - Disabling the use of bslib precompiled themes. To be able to use precompiled themes, call `options(bslib.precompiled = TRUE)`
This message is displayed once every 8 hours.
shiny devmode - Enabling warnings about low color contrasts found inside `bslib::bs_theme()`. To suppress these warnings, set `options(bslib.color_contrast_warnings = FALSE)`
This message is displayed once every 8 hours.

8.2 Shiny launch functions

In the following sections, we’ll compare shinyApp(), shinyAppDir(), and runApp() to determine which one to use in R/launch_app.R and the app.R file. As we’ve seen, the Run App/Run Shiny App buttons call runApp(), even when we have a call to shinyApp() in the app.R file. This might make you wonder,

Why even include a call to shinyApp() if the app is being launched with runApp()?

Let’s review what happens when we call shinyApp().

8.2.1 shinyApp()

One of the key features of shinyApp() is the creation of the shiny.appobj (a shiny app object):

app <- shinyApp(ui = movies_ui, 
                server = movies_server)
str(app)

If we look at the structure of the returned object from shinyApp(), we see the shiny.appobj includes the appDir under appOptions:

List of 5
 $ httpHandler     :function (req)  
 $ serverFuncSource:function ()  
 $ onStart         : NULL
 $ options         : list()
 $ appOptions      :List of 2
  ..$ appDir       : chr "/path/to/sap"
  ..$ bookmarkStore: NULL
 - attr(*, "class")= chr "shiny.appobj"
1
appDir is the first argument of runApp()
2
A shiny app object

This is why runApp() works with any .R file creating a shiny app object.

shinyApp(): Creates and launches an app defined inline within the call itself (or with UI and server functions passed as arguments).

shinyApp(
  ui = fluidPage(
    # UI elements
  ),
  server = function(input, output) {
    # Server logic
  }
)

8.2.2 shinyAppDir()

shinyAppDir() is similar to shinyApp(), but is designed to use a “path to directory that contains a Shiny app.

In practice, we can use shinyAppDir() with a returned object from shinyApp():

app <- shinyApp(
        ui = movies_ui,
        server = movies_server
      )
app$appOptions$appDir
[1] "path/to/sap"

This path can be passed to the appDir argument (along with any Shiny options).

shinyAppDir(
  appDir = app$appOptions$appDir,
  options(test.mode = TRUE)
)

shinyAppDir(): Launches an app from a directory (with an app.R or ui.R/server.R files).

shinyAppDir(
  appDir = "path/to/app/", 
  options = list())

8.2.3 runApp()

The reason we’re able to call runApp() in the Console to launch both applications above is because it’s very versatile:

runApp()

Can launch apps from ui.R/server.R, app.R or directory:

# In console
runApp()

Works with a path to an .R file that creates a shiny.appobj:5

runApp(appDir = "path/to/sap")

Can also use a shiny.appobj directly:

app <- shinyApp(
        ui = movies_ui,
        server = movies_server)
runApp(appDir = app)

This final method does not produce the loadSupport() warning because a Shiny object (shiny.appobj) has already been created, and runApp() is essentially calling print(app) in the Console.

runApp(): A generalized way to launch your app–it can run apps defined inline, or from a directory.

runApp(
  appDir = "path/to/app/",
  test.mode = TRUE/FALSE)

Recap

  1. shinyApp() is typically used to create apps within an interactive R session

  2. shinyAppDir() runs a Shiny app stored in a directory (containing an app.R file (or ui.R and server.R files)

  3. runApp() also launches apps in a directory, but it’s versatility makes it suitable for running apps in various formats, either defined inline or in separate directories.6

8.3 Standalone app function

We’ll name our standalone app function launch_app() and include the options covered below:

Launch app with the shinypak package:

launch('08_launch-app')

8.3.1 interactive()

The first function option we’ll include is a check to see if there is a “human operator to interact with” with base::interactive():

if (interactive()) {
  
} else {
   
}

You have probably seen control flow like this in shiny help file examples (like flowLayout() below):

(a) if (interactive()) in flowLayout() example
Figure 8.1: Running examples ‘interactively’ let’s us see the app demo beneath the code

Adding if and interactive() will allow our standalone app function to distinguish between 1) launching the app from a RStudio/Positron session (i.e., during development) and 2) deploying the application (i.e., like publishing the application on Posit Connect).

8.3.2 options

An options argument can be passed to shinyApp() or shinyAppDir(). For example, one of the Shiny options we could include in our standalone app function is test.mode:

Should the application be launched in test mode? This is only used for recording or running automated tests. Defaults to the shiny.testmode option, or FALSE if the option is not set.

test.mode lets us export values from our application when we’re running tests (which we’ll cover in-depth in the Tests section).

We’ll make options an argument (defaulting to an empty list()) in our updated launch_app() function.

launch_app <- function(options = list()) {
    shinyApp(
      ui = movies_ui(),
      server = movies_server,
      options = options
      )
}
1
Build the shiny app object with movies_ui and movies_server
2
Include options list

8.3.3 shiny.launch.browser

I’ve written a display_type() helper function to 1) check if the application is being run in RStudio and, if so, 2) control where the Shiny app is launched. The run argument in display_type() takes the following options:

  • "p" = Viewer Pane
  • "w" = IDE Window
  • "b" = External browser

The option to access configuration is shiny.launch.browser:7

#' Shiny app display mode helper
#'
#' @param run where to launch app: 
#'  * `"p"` = launch in viewer pane 
#'  * `"b"` = launch in external browser  
#'  * `"w"` = launch in window (default)
#'
#' @return notification of `shinyViewerType` option
#' 
#' @export
#'
display_type <- function(run = "w") {
  if (Sys.getenv("RSTUDIO") == "1") {
    switch(run,
      p = options(shiny.launch.browser = .rs.invokeShinyPaneViewer),
      b = options(shiny.launch.browser = .rs.invokeShinyWindowExternal),
      w = options(shiny.launch.browser = .rs.invokeShinyWindowViewer),
      NULL = options(shiny.launch.browser = NULL))
    environment <- "RStudio"
    shinyViewerType <- getOption('shiny.launch.browser') |> 
                        attributes() |> 
                        unlist() |> 
                        unname()
                      
    cli::cli_alert_info("App running in {environment}")
    cli::cli_alert_info("shinyViewerType set to {shinyViewerType}")
  } else {
    environment <- "RStudio"
    cli::cli_alert_info("App not running in {environment}")
  } 
}
1
Detect IDE
2
Set option
3
Print IDE
4
Print option

The cli package is imported as part the devtools/usethis workflow but we want to add it the Imports field of the DESCRIPTION with usethis::use_package('cli').

8.3.4 Updates to launch_app()

The interactive() check, display_type(), and test.mode options have been added to launch_app():

launch_app <- function(options = list(), run = "w") {
  if (interactive()) {
    display_type(run = run)
  }
    shinyApp(
      ui = movies_ui(),
      server = movies_server,
      options = options
    )
}
1
Check if interactive (Workbench) launch
2
Set shinyViewerType option with display_type()
3
Launch app

After loading, documenting, and installing our package, we can see the documentation for our updated launch_app() function in RStudio and Positron :



Ctrl/Cmd + Shift + L / D / B

Figure 8.2: Standalone app function in Positron and RStudio

In RStudio , we will confirm the app is launching without the loadSupport() warning, the message is printing to the Console from the run argument, and test.mode are implemented correctly.

We also want to confirm launch_app() works in Positron :

launch_app() launches the app in the Window

launch_app() launches the app in the Window

The updated launch_app() function launches the application in the Viewer

The updated launch_app() function launches the application in the Viewer

The updated launch_app() function in Positron

The updated launch_app() function in Positron

When we’re confident our standalone app function is working, we’ll write the app.R file.

8.4 The app.R file

The app.R file should contain any options or settings that we would use during development. There are multiple ways to launch an application from app.R, so I encourage you to explore the options below to find a method that works for your workflow/environment.

8.4.1 Non-interactive sessions

What if the session isn’t interactive?

In this case, we’ll divert all regular output to the standard error stream.

8.4.1.1 stderr()

The sink() function “diverts R output to a connection”, so the code below sends the output that would normally print to the console to the where error messages are written.

if (!interactive()) {
   sink(stderr(), type = "output")
} else {
   
}

This is useful in a non-interactive settings if we want to re-direct the error output for the console text-mode connection.

8.4.1.2 tryCatch()

tryCatch() is used for ‘catching conditions’ during the execution of an expression (expr =):

if (!interactive()) {
   sink(stderr(), type = "output")
   tryCatch(expr = {
      library(sap)
   }, error = function(e) {
      pkgload::load_all()
   })
} else {
   
}

In this case, if library(sap) throws an error, the function specified after error = is executed (i.e., pkgload::load_all()).8

Written this way, in a non-interactive R session, app.R will re-direct the error output and attempt to load and attach sap, and if this fails, app.R will attempt to load all the files in the R/ folder.

8.4.2 Interactive sessions

If the session is interactive (i.e., !interactive() == FALSE), we want app.R to load all the code in the R/ folder with pkgload::load_all().

8.4.2.1 pkgload

pkgload::load_all() is the function that’s actually called when we run devtools::load_all() (or use Ctrl/Cmd + Shift + L), and this is somewhat analogous to running library(sap)

if (!interactive()) {
   sink(stderr(), type = "output")
   tryCatch(expr = {
      library(sap)
   }, error = function(e) {
      pkgload::load_all()
   })
} else {
   pkgload::load_all()
}

The pkgload package is imported as part the devtools/usethis workflow, otherwise we’d want to add it the Imports field of the DESCRIPTION with usethis::use_package('pkgload').

8.4.2.2 withr

The withr package is designed to ‘run code with safely and temporarily modified global state’, and it comes in handy when launching shiny apps. For example, I want to turn off the loadSupport() behavior when launching the app from app.R.9

The withr package is imported as part the devtools/usethis workflow, otherwise we’d want to add it the Imports field of the DESCRIPTION with usethis::use_package('withr').

We can use withr::with_options() to accomplish this using the following new and code arguments

  1. new: a named list of the new options and their values
  2. code: the ’Code to execute in the temporary environment

We’ll place the withr::with_options() at the top of app.R and pass contents of app.R into the code argument. Just to be sure no options for shiny.autoload.r previously exist, we’ll also set this option to NULL before executing the rest of the code.

withr::with_options(new = list(shiny.autoload.r = FALSE), code = {
  if (!interactive()) {
    sink(stderr(), type = "output")
    tryCatch(
      expr = {
        library(sap)
      },
      error = function(e) {
        pkgload::load_all()
      }
    )
  } else {
    pkgload::load_all()
  }
    sap::launch_app(
      options = list(test.mode = TRUE), run = 'p')
})
1
Turn off loadSupport()
2
Define non-interactive behaviors
3
Define interactive behaviors
4
Launch app (with options)

Now that we’ve updated the app.R to account for the app-package structure, we’ll load, document, and install sap and send it’s contents to the Console:



Ctrl/Cmd + Shift + L / D / B

8.4.3 Updated app.R

When we send the contents of app.R to the Console, the loadSupport() options are applied before running the app:

Sending app.R to RStudio’s Console

Sending app.R to RStudio’s Console

Sending app.R to Positron’s Console

Sending app.R to Positron’s Console

In the documentation for loadSupport(), you’ll find a second option for removing the R/ directory sourcing behavior: placing a _disable_autoload.R file in the R/ directory (this is also one of the behaviors of the golem framework, which we will cover in the following chapters).

Both methods work–this chapter demonstrates a way to remove the loadSupport() warning without having to add this file.

Recap

This chapter has covered some options for launching your app within your app-package. We went over what to include in the standalone app function and the app.R file, the differences between shinyApp(), shinyAppDir() and runApp(), but it’s worth exploring these topics further (especially if you plan on having more than one apps in your package).

Recap: launching your app

shinyApp():

  • shinyApp() doesn’t care about file structure, so it’s useful for quick prototyping or if the app is in a single .R script. It’s also more portable because you can share your app with a single .R script.

    • It’s possible to bundle shinyApp() in a wrapper function to integrate within an R package (like we’ve done with launch_app()).

    • shinyApp() returns a shiny app object, which can be useful if you want to explore the app structure programmatically (i.e., testing).

shinyAppDir():

  • shinyAppDir() launches an app from a directory, and is useful if an app is spread across multiple files and folders.

runApp()

  • runApp() a more generalized way to launch an app. It works with:

    • apps contained in a directory (i.e., runApp('path/to/app')

    • apps in separate ui.R and server.R files (or a single app.R file)

    • a shiny app object (passed to the appDir argument).

  • Clicking on Run App will run the application with runApp() if is detects a file (or files) that creates a shiny object.

In the following chapter, we’re going to cover where to put non-standard R package files that are common to Shiny apps (images, .css/.scss file, etc.).

Please open an issue on GitHub


  1. If you recall, we covered the loadSupport() function in Chapter 2.↩︎

  2. I’ve displayed the application in the browser clicking on the ‘Open the current URL in the default browser’ icon.↩︎

  3. loadSupport() does this by sourcing the files in alphabetical order.↩︎

  4. devmode is an experimental option with multiple settings.↩︎

  5. Shiny’s examples are run using runApp()↩︎

  6. It’s worthwhile to read the documentation on shinyApp(), shinyAppDir(), and runApp().↩︎

  7. The shiny.launch.browser options are covered in this blog post by Garrick Aden-Buie, and I’ve combined them into a run argument in launch_app()↩︎

  8. pkgload is part of the conscious uncoupling of the devtools package we learned about back in Chapter 6.↩︎

  9. You can read more about the shiny.autoload.r option and loadSupport() in this article↩︎