Shiny frameworks (part 3, golem)

shiny
code
packages
Author

Martin Frigaard

Published

February 17, 2023

This is the third post in a series on building shiny apps with various frameworks. In this example, I’ll be using golem to build a ‘production-grade’ shiny app myGolemApp.

I’ve previously built 1) a ‘minimal’ shiny app (VanillaApp), and 2) a shiny app built as an R package (myPkgApp). The GitHub repo with all shiny app setups is here.

Framework comparisons

The original post that inspired this series compares ‘vanilla shiny’ (bare-bones shiny application), golem, leprechaun, and rhino across a series of dimensions (framework reliability, target type of developer, overall developing experience, etc.).

I’ll continue focusing on three technical areas: Start, Build, and Use.

  • Start covers the steps required to begin building the shiny app within the framework (from the console and IDE), and any additional packages or dependencies.

  • Build covers the development process, which includes writing and storing code, data, external resources (like CSS or JavaScript), testing, etc.

  • Use shows how developers can launch their application using the given framework/package locally (i.e., within the RStudio (Posit) IDE), common workflow tips, and anything I found confusing while building the application.

myGolemApp

golem = package

golem apps are R packages, so many of the the features in myPkgApp are available in myGolemApp (and lots of extras!).

The completed application is deployed here

Start

To create a new golem app from the console, enter the following:

install.packages("golem")
library(golem)
golem::create_golem(path = "myGolemApp")

If creating a golem app from RStudio’s New Project Wizard, the following defaults are available:

Figure 1: Creating a new golem shiny app

The initial folder structure for a new golem application is below:

myGolemApp
    ├── DESCRIPTION
    ├── NAMESPACE
    ├── R
       ├── app_config.R
       ├── app_server.R
       ├── app_ui.R
       └── run_app.R
    ├── dev
       ├── 01_start.R
       ├── 02_dev.R
       ├── 03_deploy.R
       └── run_dev.R
    ├── inst
       ├── app
       │   └── www
       │       └── favicon.ico
       └── golem-config.yml
    ├── man
       └── run_app.Rd
    ├── myGolemApp.Rproj
    ├── renv
       ├── activate.R
       ├── sandbox
       │   └── R-4.2
       └── settings.dcf
    └── renv.lock

12 directories, 17 files

dev/ scripts

The dev/ folder contains golem‘s ’guided tour’ scripts and dev/run_dev.R:

  • Below are the scripts to guide you through developing your golem app and the dev/run_dev.R code.

    • dev/01_start.R opens automatically
    myGolemApp/dev/
                ├── 01_start.R
                ├── 02_dev.R
                ├── 03_deploy.R
                └── run_dev.R
    
    1 directory, 4 files
    • dev/run_dev.R is for running the ‘development version’ of the application.

If you’ve been following along with the post in this series, you should recognize most of the items in the dev/ scripts. Even if you are familiar with R package developement, you can think of these scripts as a ‘shiny app development checklist.’

DESCRIPTION

In the dev/01_start.R script, users build a DESCRIPTION file with golem::fill_desc()

  • fill_desc() uses desc package, so the sections are entered in a key = "value" format

    • Below are the values I’ve used in myGolemApp
    Code
    golem::fill_desc(
      pkg_name = "myGolemApp",
      pkg_title = "An example goelm app",
      pkg_description = "A working example of the golem package.",
      author_first_name = "Martin",
      author_last_name = "Frigaard",
      author_email = "mjfrigaard@pm.me",
      repo_url = NULL # The URL of the GitHub Repo (optional)
    )
    • In dev/02_dev.R, the attachment::att_amend_desc() will “Amend DESCRIPTION with dependencies read from package code parsing”. If attachment is not installed, use install.package('attachment')

Package files

golem files

golem gives away lots of free code!

Both R/golem_utils_ui.R and R/golem_utils_server.R contain a lot of helper functions that come in handy if you’re tired of writing out particular function names (like reactiveValuesToList() or column(width = 6)/column(width = 12))

Check them out here:

App files

The dev/02_dev.R file covers development, so most of the golem functions are for creating files in the R/ and inst/ folders:

  • The initial R/ scripts in a new golem app:

    R/
    ├── app_config.R
    ├── app_server.R
    ├── app_ui.R
    └── run_app.R
    
    1 directory, 4 files
    • R/app_ui.R and R/app_server.R are golem’s version of ui.R and server.R

    Click on Code to view code in R/app_ui.R

    Code
    #' The application User-Interface
    #'
    #' @param request Internal parameter for `{shiny}`.
    #'     DO NOT REMOVE.
    #' @import shiny
    #' @noRd
    app_ui <- function(request) {
      tagList(
        # Leave this function for adding external resources
        golem_add_external_resources(),
        # Your application UI logic
        fluidPage(
          h1("BareBonesGolem")
        )
      )
    }
    
    #' Add external Resources to the Application
    #'
    #' This function is internally used to add external
    #' resources inside the Shiny application.
    #'
    #' @import shiny
    #' @importFrom golem add_resource_path activate_js favicon bundle_resources
    #' @noRd
    golem_add_external_resources <- function() {
      add_resource_path(
        "www",
        app_sys("app/www")
      )
    
      tags$head(
        favicon(),
        bundle_resources(
          path = app_sys("app/www"),
          app_title = "BareBonesGolem"
        )
        # Add here other external resources
        # for example, you can add shinyalert::useShinyalert()
      )
    }

    Click on Code to view code in R/app_server.R

    Code
    #' The application server-side
    #'
    #' @param input,output,session Internal parameters for {shiny}.
    #'     DO NOT REMOVE.
    #' @import shiny
    #' @noRd
    app_server <- function(input, output, session) {
      # Your application server logic
    }
    • run_app.R is an exported function that is available for me to run my app after I’ve installed the package:
    library(myGolemApp)
    myGolemApp::run_app()

Add code files

  • golem has wrappers for creating modules and helper functions in the R/ folder:

    ## Add modules ----
    ## Create a module infrastructure in R/
    golem::add_module(name = "name_of_module1", with_test = TRUE) 
    golem::add_module(name = "name_of_module2", with_test = TRUE) 
    
    ## Add helper functions ----
    ## Creates fct_* and utils_*
    golem::add_fct("helpers", with_test = TRUE)
    golem::add_utils("helpers", with_test = TRUE)
    • with_test = TRUE ensures these functions will also create test files in tests/

Configure

  • The R/app_config.R file contains two functions: app_sys() (covered above) and get_golem_config(), which reads the inst/golem-config.yml configuration file

    default:
      golem_name: myGolemApp
      golem_version: 0.0.0.9000
      app_prod: no
    production:
      app_prod: yes
    dev:
      golem_wd: !expr here::here()
    • golem-config.yml gives access to the app version, name, and (development) working directory, so it can be used to add “production-only elements” and is “shareable across golem projects”
  • golem uses app_sys() (a wrapper around system.file()) to add external resources to the application

    • app_sys() is included in the R/app_config.R file
    # Access files in the current app
    app_sys <- function(...) {
      system.file(..., package = "myGolemApp")
    }
  • get_golem_config() is also included in the R/app_config.R file

    Click on Code to view get_golem_config()

    Code
    # Read App Config
    get_golem_config <- function(
      value,
      config = Sys.getenv(
        "GOLEM_CONFIG_ACTIVE",
        Sys.getenv(
          "R_CONFIG_ACTIVE",
          "default"
        )
      ),
      use_parent = TRUE,
      # Modify this if your config file is somewhere else
      file = app_sys("golem-config.yml")
    ) {
      config::get(
        value = value,
        config = config,
        file = file,
        use_parent = use_parent
      )
    }

Testing

The tests/ folder is created in dev/01_start.R with golem::use_recommended_tests(), which is a wrapper around usethis::use_testthat()

  • golem::use_recommended_tests() adds the spelling package to our DESCRIPTION and updates the WORDLIST

  • The tests golem creates in the tests/testthat/ folder can serve as a nice guide for users new to testthat

    tests/testthat/
                ├── _snaps
                ├── test-golem-recommended.R
                ├── test-golem_utils_server.R
                └── test-golem_utils_ui.R
    
    2 directories, 4 files

External

The inst/ file initially has the following contents/structure:

inst/
  ├── WORDLIST
  ├── app/
     └── www/
         └── favicon.ico
  └── golem-config.yml 

The golem-config.yml file is covered above, but the inst/app/ folder works just like the inst/extdata folder (it is loaded when the package is installed and makes these files available to the application).

Deploy

  • The final step in the guided tour contains functions for deploying a new application to Posit Connect or Docker (it opens automatically after completing the dev/02_dev.R)

    Click on Code to view code in dev/03_deploy.R

    Code
    ## Run checks ----
    ## Check the package before sending to prod
    devtools::check()
    rhub::check_for_cran()
    
    # Deploy
    
    ## Local, CRAN or Package Manager ----
    ## This will build a tar.gz that can be installed locally,
    ## sent to CRAN, or to a package manager
    devtools::build()
    
    ## RStudio ----
    ## If you want to deploy on RStudio related platforms
    golem::add_rstudioconnect_file()
    golem::add_shinyappsio_file()
    golem::add_shinyserver_file()
    
    ## Docker ----
    ## If you want to deploy via a generic Dockerfile
    golem::add_dockerfile_with_renv()
    
    ## If you want to deploy to ShinyProxy
    golem::add_dockerfile_with_renv_shinyproxy()

    Click on Code to view the output from golem::add_shinyappsio_file()

    Code
    golem::add_shinyappsio_file()
    ── Creating _disable_autoload.R ──────────────────────────────────────────────────────
    ✔ Created
    ✔ Setting active project to '/Users/mjfrigaard/projects/myGolemApp'
    ✔ Adding '^app\\.R$' to '.Rbuildignore'
    ✔ Adding '^rsconnect$' to '.Rbuildignore'
    ✔ Adding 'pkgload' to Imports field in DESCRIPTION
    • Refer to functions with `pkgload::fun()`
    ✔ File created at /Users/mjfrigaard/projects/myGolemApp/app.R
    To deploy, run:
    • rsconnect::deployApp()
    
    • Note that you'll need to upload the whole package to ShinyApps.io
    • The app.R contents

    Click on Code to view the contents of app.R

    Code
    # Launch the ShinyApp (Do not remove this comment)
    # To deploy, run: rsconnect::deployApp()
    # Or use the blue button on top of this file
    
    pkgload::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE)
    options( "golem.app.prod" = TRUE)
    myGolemApp::run_app() # add parameters here (if any)

Build

Building an application with golem is very similar to developing an R package. New code files are placed in R/, external resources are placed in inst/, etc. I’ll cover creating modules and utility functions in the next section.

Writing code

New modules and utility functions can be created with golem::add_module() or golem::add_utils()/golem::add_fct()

  • These functions are added to the R/ folder and include @noRd by default (which must be removed create the .Rd files in the man/ folder)

    # UI module template -------------------
    #' test UI Function
    #'
    #' @description A shiny Module.
    #'
    #' @param id,input,output,session Internal parameters for {shiny}.
    #'
    #' @noRd
    #'
    #' @importFrom shiny NS tagList
    # server module template ---------------
    #' test Server Functions
    #'
    #' @noRd
  • UI module functions end with a _ui suffix:

    Click on Code to view code in R/mod_plot.R

    #' plot UI Function
    #'
    #' @param id
    #'
    #' @return shiny UI module
    #' @export mod_plot_ui
    #'
    #' @importFrom shiny NS tagList tags
    #' @importFrom shiny plotOutput verbatimTextOutput
    mod_plot_ui <- function(id) {
      ns <- shiny::NS(id)
      shiny::tagList(
        shiny::tags$br(),
        shiny::tags$blockquote(
          shiny::tags$em(
            shiny::tags$h6(
              "The code for this application comes from the ",
              shiny::tags$a("Building web applications with Shiny",
                href = "https://rstudio-education.github.io/shiny-course/"
              ),
              "tutorial"
            )
          )
        ),
        shiny::plotOutput(outputId = ns("scatterplot"))
      )
    }
  • Server module functions end with a _server suffix:

    Click on Code to view code in R/mod_plot.R

    Code
    #' plot Server Functions
    #'
    #' @param id module id
    #' @param var_inputs inputs from mod_var_input
    #'
    #' @return shiny server module
    #' @export mod_plot_server
    #'
    #' @importFrom shiny NS moduleServer reactive
    #' @importFrom tools toTitleCase
    #' @importFrom shiny renderPlot
    #' @importFrom stringr str_replace_all
    #' @importFrom ggplot2 labs theme_minimal theme
    mod_plot_server <- function(id, var_inputs) {
      shiny::moduleServer(id, function(input, output, session) {
        movies <- myGolemApp::movies
    
        inputs <- shiny::reactive({
          plot_title <- tools::toTitleCase(var_inputs$plot_title())
          list(
            x = var_inputs$x(),
            y = var_inputs$y(),
            z = var_inputs$z(),
            alpha = var_inputs$alpha(),
            size = var_inputs$size(),
            plot_title = plot_title
          )
        })
    
        output$scatterplot <- shiny::renderPlot({
          plot <- point_plot(
            df = movies,
            x_var = inputs()$x,
            y_var = inputs()$y,
            col_var = inputs()$z,
            alpha_var = inputs()$alpha,
            size_var = inputs()$size
          )
          plot +
            ggplot2::labs(
              title = inputs()$plot_title,
              x = stringr::str_replace_all(tools::toTitleCase(inputs()$x), "_", " "),
              y = stringr::str_replace_all(tools::toTitleCase(inputs()$y), "_", " ")
            ) +
            ggplot2::theme_minimal() +
            ggplot2::theme(legend.position = "bottom")
        })
      })
    }
    
    ## To be copied in the UI
    # mod_plot_ui("plot_1")
    
    ## To be copied in the server
    # mod_plot_server("plot_1")
Module names

Including mod in the name of module scripts and functions makes it easier to separate them from other functions in my package namespace, if I’m using tab-completion, or if I’m searching for a particular file using Ctrl + .:

Figure 2: Go to File/Function in RStudio

  • Include tests for new modules and functions using the with_test = TRUE argument

    tests/testthat/
                ├── _snaps
                ├── test-golem-recommended.R
                ├── test-golem_utils_server.R
                ├── test-golem_utils_ui.R
                ├── test-mod_plot.R
                ├── test-mod_plot_utils_server.R
                └── test-mod_var_input.R
    
    2 directories, 6 files

Adding resources

  • To include other files (like images), add the image file to inst/app/www/, then add the www/ to the path (see example UI code below)

    # add icon
    shiny::tags$img(src = "www/shiny.png")
  • If I wanted to include images in their own folder (like images/), I can use golem::addResourcePath() to add the name of the sub-folder to inst/app/

    # add icon
    golem::add_resource_path(
              prefix = 'images', 
              directoryPath = system.file('app/images', package = 'myGolemApp'))
  • Now I can add the image file to the inst/app/www/images/ folder and include the following code in the UI:

    # add icon
    shiny::tags$img(src = "www/images/golem-hex.png")
  • In R/app_ui.R, the app_ui() function contains the UI layout functions (fluidPage(), sidebarLayout(), etc.), and a call to golem_add_external_resources():

    Click on Code to view the updated R/app_ui.R

    Code
    #' The application User-Interface
    #'
    #' @param request Internal parameter for `{shiny}`.
    #'     DO NOT REMOVE.
    #' @import shiny
    #' @noRd
    app_ui <- function(request) {
      shiny::tagList(
        # Leave this function for adding external resources
        golem_add_external_resources(),
        # Your application UI logic
        shiny::fluidPage(
          shiny::tags$h1("myGolemApp"),
          shiny::sidebarLayout(
            shiny::sidebarPanel(
              mod_var_input_ui("vars")
            ),
            shiny::mainPanel(
              # add shiny hex in www/
              shiny::tags$img(src = "www/shiny.png"),
              mod_plot_ui("plot"),
              # add golem hex (in www/images/)
              shiny::fluidRow(
                shiny::tags$em(shiny::tags$h4(
                  "Brought to you by: ",
                  shiny::tags$img(src = "www/images/golem-hex.png")
                ))
              )
            )
          )
        )
      )
    }

    Click on Code to view golem_add_external_resources()

    Code
    # this is also included in the app_ui.R script
    golem_add_external_resources <- function() {
      add_resource_path(
        "www",
        app_sys("app/www")
      )
      tags$head(
        favicon(),
        bundle_resources(
          path = app_sys("app/www"),
          app_title = "myGolemApp"
        )
        # Add here other external resources
        # for example, you can add shinyalert::useShinyalert()
      )
    }
  • Now when I run devtools::load_all(), devtools::document(), install/restart, and load the package, I see the images properly rendered with the application:

Figure 3: Images in myGolemApp

golem takes advantage of the inst/ folder and R package structure to allow users to provide additional ‘assets’ to the application.

For example, if we use golems app_sys() function on the local directory ('.'), we see all the folders available to the application at runtime

fs::dir_tree(path = app_sys('.'))
/Library/Frameworks/R.framework/Versions/4.2/Resources/library/myGolemApp/
├── DESCRIPTION
├── INDEX
├── LICENSE
├── Meta
│   ├── Rd.rds
│   ├── data.rds
│   ├── features.rds
│   ├── hsearch.rds
│   ├── links.rds
│   ├── nsInfo.rds
│   └── package.rds
├── NAMESPACE
├── R
│   ├── myGolemApp
│   ├── myGolemApp.rdb
│   └── myGolemApp.rdx
├── WORDLIST
├── app
│   └── www
│       ├── favicon.ico
│       ├── images
│       │   └── golem-hex.png
│       └── shiny.png
├── data
│   ├── Rdata.rdb
│   ├── Rdata.rds
│   └── Rdata.rdx
├── extdata
│   └── movies.RData
├── golem-config.yml
├── help
│   ├── AnIndex
│   ├── aliases.rds
│   ├── myGolemApp.rdb
│   ├── myGolemApp.rdx
│   └── paths.rds
└── html
    ├── 00Index.html
    └── R.css

Use

If you typically build shiny apps in a single app.R file (or in ui.R and server.R files), the golem framework might seem overwhelming. I’ll give a quick overview of some areas I found confusing when I started using goelm:

  1. golem apps are run using the myGolemApp::run_app() function (included in the R/ folder)

Figure 4: Initial myGolemApp

  1. While developing, golem also comes with a run_dev function that reads the R/run_dev.R file and evaluates the code.

Click on Code to view code in dev/run_dev.R

Code
#' Run run_dev.R
#'
#' @param file File path to `run_dev.R`. Defaults to `R/run_dev.R`.
#' @inheritParams add_module
#'
#' @export
#'
#' @return Used for side-effect
run_dev <- function(
  file = "dev/run_dev.R",
  pkg = get_golem_wd()
) {

  # We'll look for the run_dev script in the current dir
  try_dev <- file.path(
    pkg,
    file
  )

  # Stop if it doesn't exists
  if (file.exists(try_dev)) {
    run_dev_lines <- readLines(
      "dev/run_dev.R"
    )
  } else {
    stop(
      "Unable to locate dev file"
    )
  }

  eval(
    parse(
      text = run_dev_lines
    )
  )
}
  1. dev/ contains golem‘s ’guided tour’ scripts (01_start.R, 02_dev.R, 03_deploy.R) and run_dev.R (covered above) - dev/ is also where to place R scripts that aren’t intended to be part of the application package.

  2. R/: the primary app files for the UI and server are stored in the R/ folder (R/app_ui.R, R/app_server.R, R/run_app.R), as well as the configuration function (R/app_config.R): - Use the R/app_config.R to configure the application to be run on different locations (computers or servers).

  3. inst/: the inst/ folder holds the golem-config.yml and location of any external app files.

The golem workflow is similar to building an R package:

  1. Deploying the application can be done with a single function: rsconnect::deployApp(), which creates the following output:

Click on Code to view the deploy output

Code
Preparing to deploy application...DONE
Uploading bundle for application: 8775458...DONE
Deploying bundle: 7054081 for application: 8775458 ...
Waiting for task: 1288644047
  building: Parsing manifest
  building: Building image: 8376474
  building: Installing system dependencies
  building: Fetching packages
  building: Building package: covr
  building: Installing packages
  building: Installing files
  building: Pushing image: 8376474
  deploying: Starting instances
  unstaging: Stopping old instances
Application successfully deployed to https://mjfrigaard.shinyapps.io/mygolemapp/

Recap

Generally speaking, golem’s start-up scripts save time and serves as a gentle introduction to some of the functions used in R package development. The golem::add_ functions are an area where (I think) golem really separates itself from standard R packages. Having dedicated shiny development functions (and the app/inst/www folder) reduces the cognitive overhead of mapping the standard R package development functions (i.e., those from usethis and devtools) into shiny-specific development.

Figure 5: myGolemApp

golem is a popular framework for a reason–it’s designed to allow developers to build a shiny application and R package simultaneously. Added bonuses include taking advantage of RStudio’s build tools, great documentation, and user-guides..