2  Shiny

Published

2024-12-20


A basic Shiny project has two files:

An app.R and .Rproj file:

shiny-app/
  ├── app.R
  └── shiny-app.Rproj

A fully developed Shiny project can have the following:

  1. An R/ folder for additional scripts (i.e., modules, helper & utility functions) that is automatically sourced when the application runs

    - The only exception to this is a global.R file, which is run first

  1. A www/ folder for external resources (images, styling, etc.) that is automatically served when the application runs

  2. An optional DESCRIPTION file that controls deployment behaviors (i.e., DisplayMode)

  3. Data files

  4. An optional README.md file for documentation

shiny-app/
  ├── DESCRIPTION
  ├── R/
     ├── module.R
     ├── helper.R
     └── utils.R
  ├── README.md
  ├── app.R
  ├── data.RData
  ├── shiny-app.Rproj
  └── www/
      └── shiny.png

*Both R/ and www/ are automatically loaded when the app launches.


This chapter briefly reviews programming with Shiny’s reactive model and how it differs from regular R programming. Then, I’ll cover some of the unique behaviors of Shiny app projects (and why you might consider adopting them if you haven’t already).

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')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '^02')

Launch apps with launch()

launch(app = '02.1_shiny-app')

Download apps with get_app()

get_app(app = '02.1_shiny-app')

Shiny basics

Reactivity is the process that lets Shiny apps respond to user actions automatically. When developing Shiny apps, we need to connect inputs, reactivity, and outputs to manage how the app behaves and predict its actions.

Shiny programming is different from regular R programming in a few important ways:

  • An Event-driven UI: Shiny apps require developers to create a user interface (UI) that helps users navigate the app. The UI registers user actions, such as button clicks or input changes, which trigger updates in the application.1

    • Regular R programming often involves executing predefined steps or functions without direct interaction or responses to user events.
  • A Reactive Server: In Shiny, the app reacts based on how inputs, values, and outputs are connected, which means that when a user makes a change, those changes are automatically shared throughout the app.

    • In standard R programming, we write functions to process data and generate outputs like graphs, tables, and model results. This method does not account for reactivity or downstream changes.

Learning reactivity can be challenging when you start, but fortunately, there are excellent tutorials and articles to help you along the way!

2.1 Shiny projects

Launch app with the shinypak package:

launch('02.1_shiny-app')

RStudio’s New Project Wizard can be used to create a new Shiny application project:

New Shiny app project

New Shiny app project

New app projects need a name and location:

We can also decide whether we want to use Git or renv

We can also decide whether we want to use Git or renv

2.1.1 Boilerplate app.R

Note that the only items in the new Shiny app project are app.R and the sap.Rproj file.

sap/
    ├── app.R
    └── sap.Rproj

1 directory, 2 files

If you’ve created a new app project in RStudio , the app.R initially contains a boilerplate application, which we can launch by clicking on the Run App button:

Click on Run App

Click on Run App
(a) Old Faithful geyser app
Figure 2.1: Boilerplate Old Faithful geyser app in new Shiny projects

The boilerplate ‘Old Faith Geyser Data’ app is a perfect example of what Shiny can do with a single app.R file, but we’ll want to exchange this code for a more realistic application.

2.2 Movies app

Launch app with the shinypak package:

launch('02.2_movies-app')

The next few sections will cover some intermediate/advanced Shiny app features using the Shiny app from the ‘Building Web Applications with Shiny’ course. This app is a great example for the following reasons:

  1. It has multiple input types that are collected in the UI

  2. The graph output can be converted to a utility function

  3. The app loads an external data file when it’s launched

  4. The code is accessible (and comes from a trusted source)

As Shiny applications become more complex, they often grow beyond just one app.R file. Knowing how to store utility functions, data, documentation, and metadata is important to manage this complexity. This preparation helps us successfully organize our Shiny apps into R packages.

2.2.1 app.R

The code below replaces our boilerplate ‘Old Faith Geyser Data’ app in app.R:

show/hide movie review Shiny app
ui <- shiny::fluidPage(
  theme = shinythemes::shinytheme("spacelab"),
  shiny::sidebarLayout(
    shiny::sidebarPanel(
      shiny::selectInput(
        inputId = "y",
        label = "Y-axis:",
        choices = c(
          "IMDB rating" = "imdb_rating",
          "IMDB number of votes" = "imdb_num_votes",
          "Critics Score" = "critics_score",
          "Audience Score" = "audience_score",
          "Runtime" = "runtime"
        ),
        selected = "audience_score"
      ),
      shiny::selectInput(
        inputId = "x",
        label = "X-axis:",
        choices = c(
          "IMDB rating" = "imdb_rating",
          "IMDB number of votes" = "imdb_num_votes",
          "Critics Score" = "critics_score",
          "Audience Score" = "audience_score",
          "Runtime" = "runtime"
        ),
        selected = "critics_score"
      ),
      shiny::selectInput(
        inputId = "z",
        label = "Color by:",
        choices = c(
          "Title Type" = "title_type",
          "Genre" = "genre",
          "MPAA Rating" = "mpaa_rating",
          "Critics Rating" = "critics_rating",
          "Audience Rating" = "audience_rating"
        ),
        selected = "mpaa_rating"
      ),
      shiny::sliderInput(
        inputId = "alpha",
        label = "Alpha:",
        min = 0, max = 1,
        value = 0.4
      ),
      shiny::sliderInput(
        inputId = "size",
        label = "Size:",
        min = 0, max = 5,
        value = 3
      ),
      shiny::textInput(
        inputId = "plot_title",
        label = "Plot title",
        placeholder = "Enter text to be used as plot title"
      ),
      shiny::actionButton(
        inputId = "update_plot_title",
        label = "Update plot title"
      )
    ),
    shiny::mainPanel(
      shiny::br(),
      shiny::p(
        "These data were obtained from",
        shiny::a("IMBD", href = "http://www.imbd.com/"), "and",
        shiny::a("Rotten Tomatoes", href = "https://www.rottentomatoes.com/"), "."
      ),
      shiny::p(
        "The data represent",
        nrow(movies),
        "randomly sampled movies released between 1972 to 2014 in the United States."
      ),
      shiny::plotOutput(outputId = "scatterplot"),
      shiny::hr(),
      shiny::p(shiny::em(
        "The code for this Shiny application comes from",
        shiny::a("Building Web Applications with shiny",
          href = "https://rstudio-education.github.io/shiny-course/"
        )
      ))
    )
  )
)

server <- function(input, output, session) {
  new_plot_title <- shiny::reactive({
    tools::toTitleCase(input$plot_title)
  }) |>
    shiny::bindEvent(input$update_plot_title,
      ignoreNULL = FALSE,
      ignoreInit = FALSE
    )


  output$scatterplot <- shiny::renderPlot({
    scatter_plot(
      df = movies,
      x_var = input$x,
      y_var = input$y,
      col_var = input$z,
      alpha_var = input$alpha,
      size_var = input$size
    ) +
      ggplot2::labs(title = new_plot_title()) +
      ggplot2::theme_minimal() +
      ggplot2::theme(legend.position = "bottom")
  })
}

shiny::shinyApp(ui = ui, server = server)

2.2.2 Utility functions

I’ve converted ggplot2 server code into a scatter_plot() utility function:

show/hide scatter_plot()
scatter_plot <- function(df, x_var, y_var, col_var, alpha_var, size_var) {
    ggplot2::ggplot(data = df,
      ggplot2::aes(x = .data[[x_var]],
          y = .data[[y_var]],
          color = .data[[col_var]])) +
      ggplot2::geom_point(alpha = alpha_var, size = size_var)

}

This function is stored in a new utils.R file:

2.2.3 Data

The movies.RData dataset contains reviews from IMDB and Rotten Tomatoes. You can download these data here. The sap project now contains the following files:

sap/
  ├── app.R
  ├── movies.RData
  ├── sap.Rproj
  └── utils.R

2 directories, 4 files

To run the movies app, we need to load the data and source the utils.R file by adding the code below to the top of the app.R file:

# install ------------------------------------
# install pkgs, then comment or remove below
pkgs <- c("shiny", "shinythemes", "stringr", "ggplot2", "rlang")
install.packages(pkgs, verbose = FALSE)

# packages ------------------------------------
library(shiny)
library(shinythemes)
library(stringr)
library(ggplot2)
library(rlang)

# data -----------------------------------------
load("movies.RData")

# utils ----------------------------------------
source("utils.R")
1
Install pkgs, then comment or remove below

Clicking on Run App displays the movie review app:

Movie review app

Movie review app

2.3 Folders

Now that we have a slightly more complex application in app.R, I’ll add a few project folders we can include in our project that have unique built-in behaviors. These folders will help organize your files and make additional resources available to your app.

Launch app with the shinypak package:

launch('02.3_proj-app')

2.3.1 R/

If your Shiny app relies on utility or helper functions outside the app.R file, place this code in an R/ folder. Any .R files in the R/ folder will be automatically sourced when the application is run.

Place utils.R in R/ folder

I’ve moved the utils.R file into the R/ folder in sap:

sap/
     └── R/
         └── utils.R

1 directory, 1 file

Shiny’s loadSupport() function makes this process possible. We’ll return to this function in a later chapter, because the R/ folder has a similar behavior in R packages.2

2.3.2 www/

When you run a Shiny application, any static files (i.e., resources) under a www/ directory will automatically be made available within the application. This folder stores images, CSS or JavaScript files, and other static resources.

Create www/ folder and download image

I’ve downloaded the Shiny logo (shiny.png) and stored it in the www/ folder.

sap/
     └── www/
           └── shiny.png

1 directory, 1 file

In the section below, we’ll reference shiny.png directly in the UI.

Following the conventional folder structure will also help set you up for success when/if you decide to convert it into an app-package.

2.4 Files

The sections below cover additional files to include in your Shiny app. None of these files are required, but including them will make the transition to package development smoother.

2.4.1 README

Including a README.md file in your root folder is a good practice for any project. Using the standard markdown format (.md) guarantees it can be read from GitHub, too. README.md files should contain relevant documentation for running the application.

Create README.md

I’ve included the content below in the README.md file

# movies app

The original code and data for this Shiny app comes from the [Building Web Applications with Shiny](https://rstudio-education.github.io/shiny-course/) course. It's been converted to use [shiny modules](https://shiny.posit.co/r/articles/improve/modules/). 

View the code for this application in the [`sap` branches](https://github.com/mjfrigaard/sap/branches/all).

2.4.2 DESCRIPTION

DESCRIPTION files play an essential role in R packages, but they are also helpful in Shiny projects if I want to deploy the app in showcase mode.

Create DESCRIPTION

I’ve included the content below in DESCRIPTION:

Type: shiny
Title: movies app
Author: John Smith
DisplayMode: Showcase                                                 
                                                                      

It’s always a good idea to leave at least one <empty final line> in your DESCRIPTION file.

After adding README.md and a DESCRIPTION file (listing DisplayMode: Showcase), the movies app will display the code and documentation when the app launches.3

2.5 Code

The following two items are considered best practices because they make your app more scalable by converting app.R into functions.

2.5.1 Modules

Shiny modules are a ‘pair of UI and server functions’ designed to compartmentalize input and output IDs into distinct namespaces,

‘…a namespace is to an ID as a directory is to a file…’ - shiny::NS() help file

Module UI functions usually combine the layout, input, and output functions using tagList(). Module server functions handle the ‘backend’ code within a Shiny server function. The UI and server module functions connect through an id argument. The UI function creates this id with NS() (namespace), and the server function uses moduleServer() to call it.

2.5.1.1 Inputs

The mod_var_input_ui() function creates a list of inputs (column names and graph aesthetics) in the UI:

show/hide mod_var_input_ui()
mod_var_input_ui <- function(id) {
  ns <- shiny::NS(id)
  shiny::tagList(
    shiny::selectInput(
      inputId = ns("y"),
      label = "Y-axis:",
      choices = c(
        "IMDB rating" = "imdb_rating",
        "IMDB number of votes" = "imdb_num_votes",
        "Critics Score" = "critics_score",
        "Audience Score" = "audience_score",
        "Runtime" = "runtime"
      ),
      selected = "audience_score"
    ),
    shiny::selectInput(
      inputId = ns("x"),
      label = "X-axis:",
      choices = c(
        "IMDB rating" = "imdb_rating",
        "IMDB number of votes" = "imdb_num_votes",
        "Critics Score" = "critics_score",
        "Audience Score" = "audience_score",
        "Runtime" = "runtime"
      ),
      selected = "imdb_rating"
    ),
    shiny::selectInput(
      inputId = ns("z"),
      label = "Color by:",
      choices = c(
        "Title Type" = "title_type",
        "Genre" = "genre",
        "MPAA Rating" = "mpaa_rating",
        "Critics Rating" = "critics_rating",
        "Audience Rating" = "audience_rating"
      ),
      selected = "mpaa_rating"
    ),
    shiny::sliderInput(
      inputId = ns("alpha"),
      label = "Alpha:",
      min = 0, max = 1, step = 0.1,
      value = 0.5
    ),
    shiny::sliderInput(
      inputId = ns("size"),
      label = "Size:",
      min = 0, max = 5,
      value = 2
    ),
    shiny::textInput(
      inputId = ns("plot_title"),
      label = "Plot title",
      placeholder = "Enter plot title"
    )
  )
}
1
y axis numeric variable
2
x axis numeric variable
3
z axis categorical variable
4
alpha numeric value for points
5
size numeric value for size
6
plot_title text

mod_var_input_server() returns these values in a reactive list:

show/hide mod_var_input_server()
mod_var_input_server <- function(id) {

  shiny::moduleServer(id, function(input, output, session) {
    return(
        reactive({
          list(
            "y" = input$y,
            "x" = input$x,
            "z" = input$z,
            "alpha" = input$alpha,
            "size" = input$size,
            "plot_title" = input$plot_title
          )
        })
    )
  })
}
1
y axis numeric variable
2
x axis numeric variable
3
z axis categorical variable
4
alpha numeric value for points
5
size numeric value for size
6
plot_title text

2.5.1.2 Display

mod_scatter_display_ui() creates a dedicated namespace for the plot output (along with some help text):

show/hide mod_scatter_display_ui()
mod_scatter_display_ui <- function(id) {
  ns <- shiny::NS(id)
  shiny::tagList(
    shiny::tags$br(),
    shiny::tags$blockquote(
      shiny::tags$em(
        shiny::tags$h6("The data 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"))
  )
}
1
Namespaced module id for plot in UI

mod_scatter_display_server() loads the movies data and collects the returned values from mod_var_input_server() as inputs(). The inputs() reactive is passed to the scatter_plot() utility function, creates the plot object, and adds the plot_title() and theme:

show/hide mod_scatter_display_server()
mod_scatter_display_server <- function(id, var_inputs) {
  shiny::moduleServer(id, function(input, output, session) {
    
    load("movies.RData")

    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 <- scatter_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")
    })
  })
}
1
loading the movies data
2
assembling the returned values from mod_var_input_server(), and creating the input() reactive
3
scatter_plot() utility function creates the plot object
4
adds the plot_title()
5
add theme to layers

Both UI and server module functions are combined into a single .R file, and all modules are placed in the R/ folder so they are sourced when the application is run.

R/
 ├── mod_scatter_display.R
 ├── mod_var_input.R
 └── utils.R

2.5.2 Standalone app function

To launch our app, we place the call to shinyApp() in a launch_app() function in app.R. Both module functions are combined in the ui and server arguments of shinyApp(). The id arguments ("vars" and "plot") connect the UI functions to their server counterparts, and the output from mod_var_input_server() is the var_inputs argument in mod_scatter_display_server().

show/hide launch_app() in app.R
# install ------------------------------------
# after installing, comment this out
pkgs <- c("shiny", "shinythemes", "stringr", "ggplot2", "rlang")
install.packages(pkgs, verbose = FALSE)

# packages ------------------------------------
library(shiny)
library(shinythemes)
library(stringr)
library(ggplot2)
library(rlang)

launch_app <- function() { 
  shiny::shinyApp(
    ui = shiny::fluidPage(
      shiny::titlePanel(
        shiny::div(
          shiny::img(
            src = "shiny.png",
            height = 60,
            width = 55,
            style = "margin:10px 10px"
            ), 
         "Movies Reviews"
        )
      ),
      shiny::sidebarLayout(
        shiny::sidebarPanel(
          mod_var_input_ui("vars")
        ),
        shiny::mainPanel(
          mod_scatter_display_ui("plot")
        )
      )
    ),
    server = function(input, output, session) {
      
      selected_vars <- mod_var_input_server("vars")

      mod_scatter_display_server("plot", var_inputs = selected_vars)
    }
  )
}
launch_app()
1
Header (comment this out after the packages are installed)
2
Load packages
3
Variable input UI module
4
Graph display UI module
5
Variable input server module
6
Graph display server module

Now, I can run the app with launch_app().

View a deployed version here

View a deployed version here

The deployed files of sap are below:

sap/ # 02.3_proj-app branch
├── DESCRIPTION
├── R/
   ├── mod_scatter_display.R
   ├── mod_var_input.R
   └── utils.R
├── README.md
├── app.R
├── movies.RData
├── sap.Rproj
├── rsconnect/
   └── shinyapps.io/
       └── user/
           └── sap.dcf
└── www/
    └── shiny.png

6 directories, 10 files

The rsconnect/ folder has been removed from the 02.3_proj-app branch.

2.6 Additional features

Below are two additional ‘optional’ features that can be included with your Shiny application. I consider these ‘optional’ because they’re use depends on the specific needs and environment for each application.

2.6.1 Globals

Placing a global.R file in your root folder (or in the R/ directory) causes this file to be sourced only once when the Shiny app launches, rather than each time a new user connects to the app. global.R is commonly used for initializing variables, loading libraries, loading large data sets and/or performing initial calculations.

I could place the header from app.R in global.R to ensure these packages are loaded before the application launches:

show/hide contents of R/global.R
# packages ------------------------------------
library(shiny)
library(shinythemes)
library(stringr)
library(ggplot2)
library(rlang)

global.R can be placed in the R/ folder

R/
├── global.R
├── mod_scatter_display.R
├── mod_var_input.R
└── utils.R

1 directory, 4 files

Or in the project root folder

├── DESCRIPTION
├── R
   ├── mod_scatter_display.R
   ├── mod_var_input.R
   └── utils.R
├── README.md
├── app.R
├── global.R
├── man
├── movies.RData
├── sap.Rproj
└── www
    └── shiny.png

4 directories, 10 files

In both locations, it will be sourced before launching the application.

global.R can be used to maintain efficiency and consistency across application sessions.

2.6.2 Project dependencies (renv)

If you use renv, keep track of your dependencies by regularly running renv::status() and renv::snapshot().

Start by initiating renv with renv::init(), then run renv::status() to check which packages have been added to the lockfile:

renv::status()
The following package(s) are in an inconsistent state:

 package      installed recorded used
 colorspace   y         n        y   
 fansi        y         n        y   
 farver       y         n        y   
 ggplot2      y         n        y   
 gtable       y         n        y   
 isoband      y         n        y   
 labeling     y         n        y   
 lattice      y         n        y   
 MASS         y         n        y   
 Matrix       y         n        y   
 mgcv         y         n        y   
 munsell      y         n        y   
 nlme         y         n        y   
 pillar       y         n        y   
 pkgconfig    y         n        y   
 RColorBrewer y         n        y   
 scales       y         n        y   
 shinythemes  y         n        y   
 tibble       y         n        y   
 utf8         y         n        y   
 vctrs        y         n        y   
 viridisLite  y         n        y   

Take a ‘snapshot’ to capture the current package dependencies:

renv::snapshot()
The following package(s) will be updated in the lockfile:

# CRAN --------------------------------------------------
- lattice        [* -> 0.21-8]
- MASS           [* -> 7.3-60]
- Matrix         [* -> 1.5-3]
- mgcv           [* -> 1.8-42]
- nlme           [* -> 3.1-162]
- vctrs          [* -> 0.6.3]

# RSPM --------------------------------------------------
- colorspace     [* -> 2.1-0]
- fansi          [* -> 1.0.4]
- farver         [* -> 2.1.1]
- ggplot2        [* -> 3.4.2]
- gtable         [* -> 0.3.3]
- isoband        [* -> 0.2.7]
- labeling       [* -> 0.4.2]
- munsell        [* -> 0.5.0]
- pillar         [* -> 1.9.0]
- pkgconfig      [* -> 2.0.3]
- RColorBrewer   [* -> 1.1-3]
- scales         [* -> 1.2.1]
- shinythemes    [* -> 1.2.0]
- tibble         [* -> 3.2.1]
- utf8           [* -> 1.2.3]
- viridisLite    [* -> 0.4.2]

Do you want to proceed? [Y/n]: y

- Lockfile written to '~/path/to/sap/renv.lock'.

2.7 Recap

This chapter has covered some differences between developing Shiny apps and regular R programming, creating new Shiny projects in Posit Workbench, and some practices to adopt that can make the transition to app-packages a little easier. The code used in this chapter is stored in the sap repository.

Recap
  • Placing utility or helper files in an R/ folder removes the need to call source() in app.R.

  • Images, CSS, JavaScript, and other static resources can be stored in www/ and Shiny will serve these files when the application is run.

  • README.md files can document the application’s description, purpose, requirements, etc.

  • DESCRIPTION files provide metadata and include fields that affect application deployment (i.e., DisplayMode: Showcase)

  • Converting the application code into functions (modules and standalone app functions) creates a ‘division of labor’ for each component, which makes it easier to think about and work on them independently.

  • Finally, if you’re using renv, run renv::status() and renv::snapshot() to manage dependencies

In the next chapter, I’ll cover what makes a package a package, and some do’s and don’ts when converting a developed Shiny application into an R package.

Please open an issue on GitHub


  1. Shiny apps require developers to design and develop a user interface (UI). User experience (UX) design is an entirely separate field, but as Shiny developers, we need to know enough to allow users to interact with and navigate our apps.↩︎

  2. Shiny introduced these features in version 1.3.2.9001, and you can read more about them in the section titled, ‘The R/ directory’ in App formats and launching apps↩︎

  3. Read more about showcase mode here↩︎