20 Packages

If you are creating a large or long-term Shiny app, I highly recommend that you organise your app in the same way as an R package. This means that you:

  • Put all R code in the R/ directory.

  • Write a function that starts your app (i.e. calls shinyApp() with your UI and server).

  • Create a DESCRIPTION file in the root directory of your app.

This structure gets your toes into the water of package development. It’s a long way from a complete package, but it’s still useful because it activates new tools that make it easier to work with larger app. The package structure will pay off further when we talk about testing in Chapter 21, because you get tools that make it easy to run the tests and to see what code is tested. In the long run, it also helps you document complex apps using roxygen2, although we won’t discuss that in this book.

It’s easy to think of packages as giant complicated things like Shiny, ggplot2, or dplyr. But packages can also be very simple. The core idea of a package is that it’s a set of conventions for organising your code and related artefacts: if you follow those conventions, you get a bunch of tools for free. In this chapter, I’ll show you the most important conventions, and then provide a few hints as to next steps.

As you start working with app-packages, you may find that you enjoy the process of package development and want to learn more. I’d suggested starting with R Packages to get the lay of the package development land, then continuing on to Engineering Shiny, by Colin Fay, Sébastien Rochette, Vincent Guyader, Cervan Girard, to learn more about the intersection of R packages and Shiny apps.

20.1 Converting an existing app

Converting an app to a package requires some upfront work. Assuming that you have an app called myApp and it already lives in a directory called myApp/, you’ll need to do the following things:

  • Create an R directory and move app.R into it.

  • Transform your app into a standalone function by wrapping:

    library(shiny)
    
    myApp <- function(...) {
      ui <- fluidPage(
        ...
      )
      server <- function(input, output, session) {
        ...
      }
      shinyApp(ui, server, ...)
    }
  • Call usethis::use_description() to create a description file. In many cases, you’ll never need to look at this file, but you need it to activate RStudio’s “package development mode” which provides the keyboard shortcuts we’ll use later.

  • If you don’t already have one, create an RStudio project by calling usethis::use_rstudio().

  • Restart RStudio and re-open your project.

You can now press Cmd/Ctrl + Shift + L to run devtools::load_all() and load all the package code and data. This means that you can now:

  • Remove any calls to source(), since load_all() automatically sources all .R files in R/.

  • If you are loading datasets using read.csv() or similar, you can instead use usethis::use_data(mydataset) to save the data in the data/ directory. load_all() automatically loads the data for you.

To make this process more concrete, we’ll next work through a simple case study before coming back to the other benefits of this work in Section 20.2.

20.1.1 Single file

Imagine I have a relatively complex app that currently lives in a single app.R:

library(shiny)

monthFeedbackUI <- function(id) {
  textOutput(NS(id, "feedback"))
}
monthFeedbackServer <- function(id, month) {
  stopifnot(is.reactive(month))
  
  moduleServer(id, function(input, output, session) {
    output$feedback <- renderText({
      if (month() == "October") {
        "You picked a great month!"
      } else {
        "Eh, you could do better."
      }
    })
  })
}

stones <- vroom::vroom("birthstones.csv")
birthstoneUI <- function(id) {
  p(
    "The birthstone for ", textOutput(NS(id, "month"), inline = TRUE),
    " is ", textOutput(NS(id, "stone"), inline = TRUE)
  )
}
birthstoneServer <- function(id, month) {
  stopifnot(is.reactive(month))
  
  moduleServer(id, function(input, output, session) {
    stone <- reactive(stones$stone[stones$month == month()])
    output$month <- renderText(month())
    output$stone <- renderText(stone())
  })
}

months <- c(
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"
)
ui <- navbarPage(
  "Sample app",
  tabPanel("Pick a month",
    selectInput("month", "What's your favourite month?", choices = months)
  ),
  tabPanel("Feedback", monthFeedbackUI("tab1")),
  tabPanel("Birthstone", birthstoneUI("tab2"))
)
server <- function(input, output, session) {
  monthFeedbackServer("tab1", reactive(input$month))
  birthstoneServer("tab2", reactive(input$month))
}
shinyApp(ui, server)

This code creates simple three page app that uses modules to keep the pages isolated. It’s a toy app, but it’s still realistic — the main difference compared to a real app is that here the individual UI and server components are much simpler.

20.1.2 Module files

Before turning it into a package, my first step is to pull the two modules out into their own files following the advice in Section 19.2.5:

  • R/monthFeedback.R:

    monthFeedbackUI <- function(id) {
      textOutput(NS(id, "feedback"))
    }
    monthFeedbackServer <- function(id, month) {
      stopifnot(is.reactive(month))
    
      moduleServer(id, function(input, output, session) {
        output$feedback <- renderText({
          if (month() == "October") {
            "You picked a great month!"
          } else {
            "Eh, you could do better."
          }
        })
      })
    }
  • R/birthstone.R:

    birthstoneUI <- function(id) {
      p(
        "The birthstone for ", textOutput(NS(id, "month"), inline = TRUE),
        " is ", textOutput(NS(id, "stone"), inline = TRUE)
      )
    }
    birthstoneServer <- function(id, month) {
      stopifnot(is.reactive(month))
    
      moduleServer(id, function(input, output, session) {
        stone <- reactive(stones$stone[stones$month == month()])
        output$month <- renderText(month())
        output$stone <- renderText(stone())
      })
    }

That leaves me with the following app.R:

library(shiny)

stones <- vroom::vroom("birthstones.csv")
#> Rows: 12
#> Columns: 2
#> Delimiter: ","
#> chr [2]: month, stone
#> 
#> Use `spec()` to retrieve the guessed column specification
#> Pass a specification to the `col_types` argument to quiet this message
months <- c(
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"
)

ui <- navbarPage(
  "Sample app",
  tabPanel("Pick a month",
    selectInput("month", "What's your favourite month?", choices = months)
  ),
  tabPanel("Feedback", monthFeedbackUI("tab1")),
  tabPanel("Birthstone", birthstoneUI("tab2"))
)
server <- function(input, output, session) {
  monthFeedbackServer("tab1", reactive(input$month))
  birthstoneServer("tab2", reactive(input$month))
}
shinyApp(ui, server)

Just pulling the modules out into separate files is useful because it helps me understand the big picture of the app. If instead I want to dive into the details, I can look at the modules files.

20.1.3 A package

Now let’s make this into a package. First I run usethis::use_description(), which creates a DESCRIPTION file. Next, I move app.R to R/app.R and wrap shinyApp() into a function:

library(shiny)

monthApp <- function(...) {
  stones <- vroom::vroom("birthstones.csv")
  months <- c(
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
  )
  
  ui <- navbarPage(
    "Sample app",
    tabPanel("Pick a month",
      selectInput("month", "What's your favourite month?", choices = months)
    ),
    tabPanel("Feedback", monthFeedbackUI("tab1")),
    tabPanel("Birthstone", birthstoneUI("tab2"))
  )
  server <- function(input, output, session) {
    monthFeedbackServer("tab1", reactive(input$month))
    birthstoneServer("tab2", reactive(input$month))
  }
  shinyApp(ui, server, ...)
}

As an optional extra, I converted birthstones.csv to a package dataset by running usethis::use_data("birthstones"). This creates data/birthstones.rda, which will be loaded automatically when I load the package. I can now delete birthstones.csv and remove the line that reads it in: stones <- vroom::vroom("birthstones.csv").

You can see the final product at https://github.com/hadley/monthApp.

20.2 Benefits

Why bother doing all this work? The most important benefit is a new workflow that makes it easier accurately re-load all app code and relaunch the app. But it also makes it easier to share code between apps and share your app with others.

20.2.1 Workflow

Putting your app code into the package structure unlocks a new workflow:

  • Re-load all code in the app with Cmd/Ctrl + Shift + L. This calls devtools::load_all() which automatically saves all open files, source()s every file in R/, loads all datasets in data/ then puts your cursor in the console.

  • Re-run the app with myApp().

As your app grows bigger, it’s also worth knowing about the two most important code navigation keyboard shortcuts:

  • Ctrl/Cmd + . will open the “fuzzy file and function finder” — type a few letters at the start of the file or function that you want to navigate to, select it with the arrow keys and then press enter. This allows you to quickly jump around your app without taking your hands off the keyboard.

  • When your cursor is on the name of function, F2 will jump to the function definition.

If you do a lot of package development, you might want to automatically load usethis, so you can type (e.g.) use_description() instead of usethis::use_description(). You can do so by adding the following lines to your .Rprofile. This file contains R code that’s run whenever you start R, so it’s a great way to customise your interactive development environment.

if (interactive()) {
  require(usethis, quietly = TRUE)
}

The easiest was to find and open your .Rprofile is to run usethis::edit_r_profile().

20.2.2 Sharing

Since your app is now wrapped up in a function, it’s easy to include multiple apps in the same package. And because you have multiple apps in the same place, it’s now much easier to share code and data across apps. That’s a huge benefit if you have a bunch of apps for related tasks.

Packages are also a great way to share apps. shinyapps.io and RStudio Connect are great way to share apps with folks who aren’t familiar with R. But sometimes you want to share apps with your colleagues who do use R — maybe instead of allowing the user to upload a dataset, you want to provide them with a function that they call with a data.frame. For example, the following very simple app allows the R user to supply their own dataframe for interactive summaries:

dataSummaryApp <- function(df) {
  ui <- fluidPage(
    selectInput("var", "Variable", choices = names(df)),
    verbatimTextOutput("summary")
  )
  
  server <- function(input, output, session) { 
    output$summary <- renderPrint({
      summary(df[[input$var]])
    })
  }
  
  shinyApp(ui, server)
}

RStudio Gadgets build on this idea: they are Shiny apps that let you add new user interface to the RStudio IDE. It’s even possible to write gadgets that generate code, so you can perform some task that’s easy to do interactively, and the gadget generates the corresponding code and saves back into the open file.

20.3 Extra steps

There are two common extra steps you might take beyond the basics: making it easy to deploy your app-package, and turning it into a “real” package.

20.3.1 Deploying your app-package

If you want to deploy your app to RStudio Connect or Shiny59 you’ll need two extra steps:

  • You’ll need an app.R that tells the deployment server how to run your app. The easiest way is to load the code with pkgload:

    pkgload::load_all(".")
    myApp()

    (You can see other techniques at https://engineering-shiny.org/deploy.html).

  • Normally when you deploy an app, the rsconnect package automatically figures out all of the packages your code uses. But now that you have a DESCRIPTION file, it requires you to explicitly specify them. The easiest way to do this is to call usethis::use_package(). You’ll need to start with shiny and pkgload:

    usethis::use_package("shiny")
    usethis::use_package("pkgload")

    This is a little a more work, but the payoff is a having an explicit list of every package that your app needs in one place.

Now you can run rsconnect::deployApp() whenever you’re ready to share an updated version of your app with your users.

20.3.2 R CMD check

A minimal package contains an R/ directory, a DESCRIPTION file, and a function to run your app. As you’ve seen, this is already useful because it unlocks some useful workflows to speed up app development. But what makes a “real” app? To me, it’s making a serious effort to get R CMD check passing. R CMD check is R’s automated system that checks your package for common problems. In RStudio, you can run R CMD check by pressing Cmd/Ctrl + Shift + E.

I don’t recommend that you do this the first time, the second time, or even the third time you try out the package structure. Instead, I recommend that you get familiar with the basic structure and workflow before you take the next step to make a fully compliant package. It’s also something I’d generally reserve for important apps, particularly any app that will be deployed elsewhere. It can be a lot of work to get R CMD check passing, and there’s little pay off in the short term. But in the long-term this will protect you against a number of potential problems, and because it ensures your app adheres to standards that R developers are familiar with, it makes it easier for others to contribute to your app.

Before you make your first full app-package, you’ll should read “The whole game” chapter of R packages: it will give you a fuller sense of the package structure, and introduce you to other useful workflows. Then use the following hints to get R CMD check passing cleanly:

  • Remove any calls to library() or require() and instead replace them with a declaration in your DESCRIPTION. usethis::use_package("name") to add the required package to the DESCRIPTION60. You’ll then need to decide whether you want to refer to each function explicitly with ::, or use @importFrom packageName functionName to declare the import in one place.

    At a minimum, you’ll need usethis::use_package("shiny"), and for Shiny apps, I recommend using @import shiny to make all the functions in the Shiny package easily available. (Using @import is not generally considered best practice, but it makes sense here).

  • Pick a license and then use the appropriate use_license_ function to put it in the right place. For proprietary code you can use usethis::use_proprietary_license(). See https://r-pkgs.org/license.html for more details.

  • Add app.R to .Rbuildignore with usethis::use_build_ignore("app.R") or similar.

  • If your app contains small reference datasets, put them in data or inst/extdata. We discussed usethis::use_data() above; alternatively, you can put raw data in inst/ext and load it with read.csv(system.file("exdata", "mydata.csv", package = "myApp")) or similar.

  • You can also change your app.R to use the package. This requires that your package is available somewhere that your deployment machine can install from. For public work this means a CRAN or GitHub package; for private work this may mean using a tool like RStudio Package Manager or drat.

    myApp::myApp()

20.4 Summary

In this chapter, you’ve dipped your toes into the water of package development. This might seem overwhelming if you think of packages like ggplot2 and shiny, but packages can be very very simple. In fact, all a project needs to be a package is a directory of R files and a DESCRIPTION file. A package is just a lightweight set of conventions that unlock useful tools and workflows. In this chapter, you learned how to turn an app into a package and some of the reasons you might want to. In the next chapter, you’ll learn about the most important reason to turn your app into a package: to make it easier to test.