Production-Grade R Shiny with Golem: Prototyping | by Liz Nelson | Towards Data Science

It’s an exciting time to be an R Shiny developer — R Shiny is being increasingly adopted to let data scientists make fast, beautiful web applications to share data and even provide public-facing access to data products and dashboards. R Shiny lets non-web developers get applications up and running quickly, but also puts data scientists and data analysts in the position of becoming web developers or software engineers in terms of dependency management, reproducibility, testing, and deployment responsibilities. As R Shiny has been adopted in various spheres, a new world of production Shiny tools have been developed to help meet the challenges of developing robust, sustainable, and reproducible R Shiny applications created and maintained by data scientists and non-web developers. In fact, I would argue that even if you don’t think of your application as truly production or software, you too can benefit from using production Shiny tools in your development workflow.
In this tutorial I will cover the basics of getting an R Shiny application up and running in development with a new package called Golem, which is particularly designed as an opinionated framework for developing production-ready R Shiny applications. For the purpose of this tutorial, our client is me, and we’ll be focused on prototyping a weightlifting application.
In this tutorial, you will learn:
Basic ideas of user interviewing and prototyping
Basic wireframe drawing
How to make a working prototype in R Shiny with filler text and demo graphics using the shinipsum package
You may be wondering why this section is here — come on, get to the code! Well, before we start muddling around in R, we should probably take a moment to figure out what we’re trying to do! This is perhaps the most underrated development step of them all: actually defining what we want to build. Luckily, given that the user of this application is me (or us), this should be a pretty easy user interview!
Core Functionality
Upload updated exports from Fitbod (where I generate and track my workouts)
Track how much weight I have lifted over time to feel accomplished
Track how much total volume per workout I have lifted in different exercises
Track my projected 1 rep max for different exercises
Track how much I have lifted per muscle group
One last step before we get to coding, I promise. We’re going to do a quick (manual) sketch of what we’re going to build so we have an idea of what we’re going for. There are lots of cool tools for interactive mockups, if you are so inclined, but we’re going to keep it simple for now and just draw it out. I drew this one out on a tablet, but pen and paper works just fine as well.
First, let’s draw what we want our main dashboard page and the data upload to look like:
We can also draw two more tabs to include the content on exercise-specific statistics and muscle-group statistics outlined in our user interview.
Starting a project with Golem is deceptively simple. First, make sure you have it installed using your good old friend install.packages('golem').
After you’ve installed golem, you should be able to go to ‘File’ -> ‘New Directory’ -> ‘Package for Shiny App using golem’ in your RStudio menu.
You can also create your project on the command line by typing golem::create_golem("projectName") in your RStudio console.
From here, Golem will open up lots of files to get you going! Let’s start with dev/01_start.R at the comment numbered 1.1. Go ahead and fill out the golem::fill_desc() function with your name, the package name, etc. I prefer to set up my repos ahead of time, so I’m going to create an empty repo on GitHub and input the URL here as well.
Now let’s call golem::set_golem_options() to set all this up. We’re also going to keep everything in comment ‘1.2 — Set common files’ as it is for now.
We’re now going to run golem::use_recommended_tests() to add a folder for unit tests we can add later using testthat. Testing allows you to automate the process you use to ensure your code is working correctly. Why should you use unit testing? Well, this says it pretty well:
I started using automated tests because I discovered I was spending too much time re-fixing bugs that I’d already fixed before. While writing code or fixing bugs, I’d perform interactive tests to make sure the code worked. But I never had a system which could store those tests so I could re-run them as needed. I think that this is a common practice among R programmers. It’s not that you don’t test your code, it’s that you don’t automate your tests.
From R Packages by Hadley Wickham and Jennifer Bryan
Now we run golem::use_recommended_deps() to add Golem’s recommended dependencies. These are packages you are almost certain to need later in your development process.
I’m also going to run golem::use_utils_ui() and golem::use_utils_server() because I'm never one to say no to some nice pre-written functions I'm likely to need later. These aren't strictly necessary, but they add some functions for common R Shiny tasks that might help us out.
With that, we’re ready to get prototyping!
Remember that drawing we made way back when of what we wanted to make? Well, now it’s time to see if it looks any good when we actually try to make it in Shiny!
Prototyping is a great way to be able to pulse-check your user requirements before you get all the way through building something. When people can see what an app looks like and play with it, even if it doesn’t have the final information yet, they tend to clarify what they want as they realize the application is missing something, or perhaps they don’t like or want a feature they thought would be essential.
We’re going to try out a fun new part of the golem-universe to do our prototyping: {shinipsum}
As always, we’ll start with installing it: install.packages('shinipsum') and we'll let golem know we'll be using it with usethis::use_package("shinipsum").
Let’s start prototyping!
If we look back at our diagrams, we see we really had three tabs (I’m not counting our initial pop-up to have our user upload their data).
This means that having three modules, one for each page, is probably a good start.
Let’s create our first module using golem! In your R terminal, you can use golem’s functions to directly add module infrastructure:
golem::add_module(name = 'Home')
Now let’s add our other modules:
golem::add_module(name = 'Exercises')
golem::add_module(name = 'MuscleGroup')
Cool- we’ve got two modules! Let’s do as it says in our newly created modules and add the references to these modules in the main UI and main server, so the application knows to call them. Once you’ve pasted those chunks into the right spot in the R/app_ui.R and the R/app_server.R files, feel free to delete the commented chunk at the bottom of your module files.
Now let’s look back at the drawing of our first page to start working on our modules.
Hey, that navigation bit looks pretty important, so let’s focus on that first. We want this to look pretty, so we’re going to bring out the big guns of gorgeous Shiny UI design:
install.packages("shinydashboard")install.packages("shinydashboardPlus")usethis::use_package("shinydashboard")usethis::use_package("shinydashboardPlus")
We’re going to put our sidebar in our main R/app_ui.R file, because we'll want it visible on all our pages. Now hold with me here, because this is going to be a wild ride of UI code for a minute.
app_ui <- function() {  tagList(    # Leave this function for adding external resources    golem_add_external_resources(),    # Define this page as a dashboard page to signal we're using the     dashboard page format    shinydashboardPlus::dashboardPagePlus(            header = shinydashboardPlus::dashboardHeaderPlus(        title = "Fitbod Tracking Dashboard",        enable_rightsidebar = FALSE      ),      # Create our navigation menu that links to each of the tabs we defined      sidebar = shinydashboard::dashboardSidebar(        shinydashboard::sidebarMenu(          # Setting id makes input$tabs give the tabName of currently-selected tab          id = "tabs",          shinydashboard::menuItem("Dashboard", tabName = "dashboard", icon = icon("dashboard")),          shinydashboard::menuItem("Muscle Group View", icon = icon("th"), tabName = "mg"),          shinydashboard::menuItem("Exercise View", icon = icon("bar-chart-o"), tabName = "ev"          ))      ),# Show the appropriate tab's content in the main body of our dashboard when we select it      body = shinydashboard::dashboardBody(        shinydashboard::tabItems(          shinydashboard::tabItem("dashboard", mod_Home_ui("Home_ui_1")),          shinydashboard::tabItem("mg", mod_MuscleGroup_ui("MuscleGroup_ui_1")),          shinydashboard::tabItem("ev", mod_Exercises_ui("Exercises_ui_1")          )        )       ),      rightsidebar = NULL,      title = "FitBod App Monitoring Dashboard"    )  )}
Just for fun, let’s also add something to each of our module pages so we can tell if our navigation is working.
In each of your R/mod_TABNAME_ui.R files, add a little line to signify which tab is showing after the tagList(
For example:
mod_Home_ui <- function(id){  ns <- NS(id)  tagList(  h1("Welcome HOME!")  )}
Now let’s see if this works! If you are a more experienced Shiny developer, you may have noticed we’re not sourcing our modules anywhere, and there’s no ‘Run App’ button on any of our scripts. Those pieces are both integrated into the Golem infrastructure! So how do we run our app? Let’s navigate to the dev/run_dev.Rscript.
Hey, that run_app() command looks familiar! Since we're building our app as a package in golem, we run the run_app() function out of our own package. Let's go ahead and run this script!
Assuming everything worked correctly, we should see something like this pop up:
Excellent! Let’s work on fleshing out these pages now.
So let’s put {shinipsum} to use so we don't have to worry about the data for now. We will need good old {ggplot2} though, so if you don't have it installed, please install it and remember to also use the command usethis::use_package("ggplot2") in your R terminal so golem knows to load the package when it runs our app.
In our R/mod_Home.R file, we can change our placeholder text to look more like what we expect to see and add placeholder graphs and tables as well.
A few notes:
When you call functions in a golem app, you need to make sure you attach the package using the :: notation. This is a good practice anyway as it makes sure you're using the right function from the right package when functions have clashing names.
We’re using the shinydashboard::box() function to give us nice formatted boxes with minimal work-- that's one of the great advantages of these packages!
Shinipsum gives us automatic fake data to make our plots with so we don’t have to worry about our data yet.
We’re using modules, so we need the ns() around our output tags so our namespacing will work right.
mod_Home_ui <- function(id){  ns <- NS(id)  tagList(    fluidPage(      fluidRow(        column(width = 5,          h2("Welcome!")),          br(),br(),br()        ),      fluidRow(        column(width = 10,          shinydashboard::box(            background = "blue",            h2("You lifted XYZ xx times!"),            title = "Congrats!"),              br(), br(), br(), br()          )),      fluidRow(          shinydashboard::box(                 title = "Top Exercises",                 width = 6,                 DT::dataTableOutput(ns('data_table'))),          shinydashboard::box(            title = "Total Weight Lifted",            width = 6,            plotOutput(ns("plot"))      )    )  )  )}mod_Home_server <- function(input, output, session){ ns <- session$ns  output$data_table <- DT::renderDT({   shinipsum::random_DT(5, 3, "numeric") })  output$plot <- renderPlot({   shinipsum::random_ggplot(type = "line") })}
Now if you run your app again (remember, we do this from the dev/run_dev.Rscript now) it should look like this:
That’s looking much better! Time to finish up the prototypes of our other two tabs. Remember, our exercise and muscle group tabs should look like this (according to our wireframes):
Luckily, we can reuse a fair amount of our code from our dashboard page. So in R/mod_Exercises.R we will flesh out this demo UI!
A few notes:
We have hard-coded the choices in our input box for now, but we’ll fill this with our data later!
You’ll notice we’re using boxes again, as well as a Shiny function to create a spot for our users to filter based on what exercise they want to look at.
mod_Exercises_ui <- function(id){  ns <- NS(id)  tagList(    fluidPage(      fluidRow(          h2("Exercise Progress for "),          selectizeInput(            inputId = ns("exercises"),             label = "",            # Hard coded for now, but we'll deal with this later!            choices = c("Dumbell Bicep Curl", "Deadlift"),            selected = "Dumbell Bicep Curl",            width = '50%',            multiple = FALSE)      ),      fluidRow(        column(width = 10,               shinydashboard::box(                 background = "blue",                 h2("Your current projected 1 rep max is x!"),                 title = "Congrats!")        )),      fluidRow(        shinydashboard::box(          title = "Total Weight Per Workout",          width = 6,          plotOutput(ns("plot2"))),        shinydashboard::box(          title = "Max Weight Per Workout",          width = 6,          plotOutput(ns("plot3"))          )      )      )    )}mod_Exercises_server <- function(input, output, session){  ns <- session$ns    output$plot2 <- renderPlot({    shinipsum::random_ggplot(type = "line")  })    output$plot3 <- renderPlot({    shinipsum::random_ggplot(type = "line")  })}
Great! If you run your app, your ‘Exercises’ tab should now look like this:
I bet you can guess what we’re going to do on the muscle groups tab! Try and do that one yourself based on what we’ve already written, but if you need help, my code is below.
In R/mod_MuscleGroup.R:
mod_MuscleGroup_ui <- function(id){  ns <- NS(id)  tagList(    fluidPage(      fluidRow(        h2("Muscle Group Progress for "),        selectizeInput(          inputId = ns("muscles"),           label = "",           choices = c("Back", "Hamstrings"),          selected = "Back",          width = '50%',          multiple = FALSE)      ),      fluidRow(        shinydashboard::box(          title = "Max Weight Over Time",          width = 6,          plotOutput(ns("plot4"))),        shinydashboard::box(          title = "Top Exercises",          width = 6,          DT::dataTableOutput(ns('data_table2'))          )        )    )  )}mod_MuscleGroup_server <- function(input, output, session){  ns <- session$ns    output$plot4 <- renderPlot({    shinipsum::random_ggplot(type = "line")  })    output$data_table2 <- DT::renderDT({    shinipsum::random_DT(5, 3, "numeric")  })}
The final result should look like this:
Now we have a fully functioning prototype ready to show our theoretical user (in this case, us)! We can make alterations based on our user’s feedback much more easily using prototyping graphs rather than dealing with all our data wrangling, and we can get a prototype to our user much more quickly when we don’t have to make the whole app work before they see anything. Once our user approves the final wireframe, we’ll be ready to build the rest of our Shiny app the normal way and we’re all set up to use automated testing and Docker-based deployments using the Golem infrastructure.
If you’ve had any trouble following along, all of the code for this tutorial is in my Github repository here.