St. Jude members may view an interactive form of this document here. Must be connected to the St. Jude network to view.

Common Gotchas and Debugging Challenges

Debugging, the process of finding and fixing bugs, is a critical skill when working with Shiny apps, just like with any other programming.

There are many “gotchas” that beginner Shiny developers can run into, and these are typically more difficult to troubleshoot than typical R code for a few reasons.

These debugging challenges stem from:

  • Reactivity: Shiny operates on a reactive programming model, which can be quite different from the standard procedural programming model common in R. In reactive programming, outputs are automatically updated based on changes to inputs. This can make it harder to reason about the order in which different parts of your code are executed, especially for beginners.

  • Asynchronous operations: Shiny apps often involve asynchronous operations, such as user input or data loading. These operations can occur at any time and in any order, which can make bugs harder to reproduce and track down.

  • Reactive context: Reactive expressions in Shiny apps can only be evaluated in a reactive context. Trying to access reactive values outside of this context can lead to errors. This restriction can make it challenging to debug Shiny apps using traditional methods, like print statements or the browser console.

  • Multiple environments: Shiny applications have multiple environments (user-interface environment, server environment) where the code runs. This can lead to issues if you’re not careful about where variables are defined and accessed.

  • Concurrency issues: Shiny apps can be accessed by multiple users at the same time, and each user has their own session. This can lead to concurrency issues, which can be very difficult to debug. For example, a bug might only occur when two users interact with the app in a certain way at the same time.

  • Deployment-related issues: Problems may arise when deploying Shiny apps that were not present during local development. These can be due to differences in the environments, such as different R versions available or missing system dependencies on the host.

Debugging Shiny Apps

Here are a few techniques you can use to debug your Shiny apps:

  • The browser() function: This function pauses execution of your app and lets you interactively run and inspect code. You can step through your code line by line and examine the current state of variables and reactives.

For example:

library(shiny)

ui <- fluidPage(
    titlePanel("Debugging Shiny Apps"),
    sidebarLayout(
        sidebarPanel(
            numericInput("num", "Enter a number", value = 1)
        ),
        mainPanel(
            textOutput("result")
        )
    )
)

server <- function(input, output) {
    output$result <- renderText({
        browser()
        x <- 1 / as.numeric(input$num)
        paste("The reciprocal is", x)
    })
}

shinyApp(ui, server)

When the browser() line is hit, you’ll be able to step through the code and see the value of each variable at each step.

Consider this your bread and butter for debugging Shiny apps and R code in general.

  • Using the react log: This method uses the reactlog package to log reactive events in your app. This can be very helpful to see when and why your reactive expressions are firing.

To see how it works, launch our simple app and change the input value a few times.

library(reactlog)
reactlog_enable() # This turns on reactive logging for Shiny apps in the R session.

server <- function(input, output) {
    output$result <- renderText({
        x <- 1 / as.numeric(input$num)
        paste("The reciprocal is", x)
    })
}

shinyApp(ui, server)

Now open the react log.

shiny::reactlogShow()

This is useful when you’re unsure of when and why your reactives are firing.

  • Chuck It at an LLM: Large language models (like chatGPT) trained on large amounts of code are often very good at finding bugs in code. They may not always have the fix right away, but they can often point you in the right direction. This is especially true with the latest models like GPT4. For more complex applications, you’ll still want to learn the above debugging tools, but for simple apps or singular functions, this can be a quick way to find bugs.

  • The reactiveConsole() function: This one isn’t necessarily a debugging tool, but it’s useful for playing around with reactives in the console. This function allows you to interactively run code that involves reactive values or expressions. Normally, you can’t do this because reactives only work within the correct reactive context. But when you call reactiveConsole(TRUE), you can run this code directly from the console.

This is an experimental feature currently, but it’s useful for playing around with reactives in the console.

For example:

reactiveConsole(TRUE)

x <- reactiveVal(10)
y <- observe({
    message("The value of x is ", x())
})
x(20)
x(30)

reactiveConsole(FALSE)

This is useful when you want to experiment with reactives to see how they interact.

Remember, debugging is a skill that takes practice to develop, and these tools will help you identify and fix issues in your Shiny apps more easily.

An Exercise in Debugging

Here’s a common “gotcha” scenario involving reactivity and debugging in R Shiny.

Consider the following simple Shiny app:

library(shiny)

ui <- fluidPage(
    textInput("inputText", "Enter some text:"),
    actionButton("goButton", "Go"),
    textOutput("outputText")
)

server <- function(input, output) {
    observeEvent(input$goButton, {
        output$outputText <- renderText({
            paste("You entered:", input$inputText)
        })
    })
}

shinyApp(ui = ui, server = server, options = list(height = 200))

In this app, the user is supposed to enter some text, then click the “Go” button to display the entered text. However, when you run this app and try to use it, you’ll find that the “Go” button doesn’t seem to do anything beyond the first press. The output text doesn’t update when you click the button, but when you input the number.

Take a close look at the code. Have any ideas about what’s going on?

Using browser() to Debug

Let’s try using our newfound friend, the browser function, to see if we can figure out what’s going on.

First, we have to add the browser() function to our code. Execution will pause at whatever point we place it, and we’ll be able to explore the environment or step through the code line by line.

Take a second to think about where it makes the most sense to place the browser() function in this app.

You can use as many browser calls as you’d like - execution will pause at each, but usually you’ll only need one or two near where the problem is occurring.

ui <- fluidPage(
    textInput("inputText", "Enter some text:"),
    actionButton("goButton", "Go"),
    textOutput("outputText")
)

server <- function(input, output) {
    observeEvent(input$goButton, {
        output$outputText <- renderText({
            browser() # Add browser() call here
            paste("You entered:", input$inputText)
        })
    })
}

shinyApp(ui = ui, server = server, options = list(height = 200))

When you run this app, enter some text, and click the “Go” button, the app will pause and you’ll be put into browser mode. You can now inspect the environment at this point in the execution. For example, you could check the value of input$inputText.

Click “Continue” to resume execution, then change the input values. This will result in browser being called again.

In this case, the browser() function can help you understand that renderText is being recomputed even when the button isn’t pressed. In general, browser() is a great tool for understanding the flow of a Shiny app and figuring out where things are going wrong. It’s especially useful in more complex apps where the control flow and reactivity can be difficult to follow.

The Issue

The mistake here is a misunderstanding about how reactivity works in Shiny.

observeEvent depends on input$goButton, but the expression passed to it is not isolated.

That means that if it uses or depends on reactive values, it will be re-executed whenever those values change, regardless of whether the event has occurred. The renderText function includes input$inputText, which is a reactive value, so renderText will be re-executed whenever the value of input$inputText changes.

There are a few ways to fix this:

  1. Use isolate() to prevent renderText from being re-executed when input$inputText changes.

isolate is a function that prevents reactive values from triggering recomputation of a reactive expression. It is very useful for situations like this where you want output to be updated only when a certain event occurs.

Solution 1
ui <- fluidPage(
    textInput("inputText", "Enter some text:"),
    actionButton("goButton", "Go"),
    textOutput("outputText")
)

server <- function(input, output) {
    observeEvent(input$goButton, {
        output$outputText <- renderText({
            # Note wrapping of input$inputText in isolate()
            paste("You entered:", isolate(input$inputText))
        })
    })
}

shinyApp(ui = ui, server = server, options = list(height = 200))
  1. Get rid of the observeEvent and check the value of input$goButton inside of renderText, again coupled with isolate.
Solution 2
ui <- fluidPage(
    textInput("inputText", "Enter some text:"),
    actionButton("goButton", "Go"),
    textOutput("outputText")
)

server <- function(input, output) {
    output$outputText <- renderText({
        # Action buttons have a counter that increments on click.
        # This will prevent the text from updating until the button is clicked.
        if (input$goButton == 0) {
            return(NULL)
        }
        paste("You entered:", isolate(input$inputText))
    })
}

shinyApp(ui = ui, server = server, options = list(height = 200))

The second solution is a more common pattern for using action buttons than the first solution. It’s easier to follow and doesn’t require an additional reactive expression in the form of the observeEvent call.

The isolate function is used to prevent input$inputText from triggering recomputation on its own, so the text will only update when the button is clicked.

This example illustrates a common misunderstanding about Shiny’s reactivity model, and also demonstrates the kind of thinking required to debug a Shiny app. Debugging in Shiny can be challenging because the code execution is event-driven and asynchronous, which is quite different from the usual top-to-bottom flow of an R script.

App Deployment

After you’ve built your Shiny application, the next step is to share it with the world. Or your lab/group. There are several ways to do this, ranging from simple and free solutions for small apps, to more complex and robust methods for large-scale applications.

In this section, we’ll cover two popular deployment options: shinyapps.io and Posit (formerly RStudio) Connect.

Shinyapps.io

Shinyapps.io is a service provided by RStudio for hosting Shiny applications. It’s an easy way to publish your applications to the web without needing to manage a server.

Key features:

  • Simple Deployment: You can publish your apps directly from your RStudio IDE with a few clicks.

  • Scalability: Shinyapps.io can automatically manage the server resources for your app, scaling it up and down as needed based on traffic.

  • Authentication and User Management: With shinyapps.io, you can control who can access your applications. It also supports Google Authentication and GitHub.

  • Analytics: Shinyapps.io provides usage statistics for your applications, so you can see how many people are using it, when, and from where.

Limitations:

  • The free tier of shinyapps.io has limitations on the number of active hours your apps can have in a month, the number of applications you can deploy, and the resources each can use (e.g. 4GB memory) max.

While shinyapps.io is great for small to medium-sized applications, for large-scale enterprise applications, you might need more control over your server environment.

How to Deploy to Shinyapps.io

The process is simple, and there are easy to follow instructions here.

Posit Connect

Posit Connect is a more commerical, professional publishing platform. It is designed to help you share Shiny applications, R Markdown reports, Plumber APIs, data Pins, and more with your colleagues and organization.

Key features:

  • Flexible Deployment: You can deploy your apps on your own servers, giving you more control over the environment.

  • Security and Access Control: RStudio Connect offers robust security features, including integration with many enterprise authentication systems. You can control who can access your applications at a very granular level.

  • Schedule Reports: One of the most powerful features of RStudio Connect is the ability to schedule R Markdown reports and have them emailed to a list of recipients.

  • Manage Multiple R Versions: RStudio Connect supports multiple versions of R concurrently. This can be crucial for reproducibility in team settings.

Limitations:

  • RStudio Connect is a commercial product, and it comes with a hefty cost. Fortunately, St. Jude has a liecense, so we don’t have to worry about said cost.

  • Setting up RStudio Connect requires more technical expertise than using shinyapps.io. You’ll need to install and manage it on your own server. However, once it’s set up, it’s very easy to deploy applications from RStudio. You can put in a serviceNow request to have a VM spun up and Posit Connect installed on it.

  • Making apps publically available is a bit more complicated than shinyapps.io, and St. Jude IT/legal has concerns if data is to be uploaded to the server.

How to Deploy to Posit Connect

This process is just a little more complicated than shinyapps.io. First, you need a Posit Connect server to which you have publishing permissions.

At St. Jude, you can put in a ticket to have a Posit Connect server spun up for you. They will provide you with a URL that you will login to with your St. Jude credentials.

To publish to this server, you’ll need to install the rsconnect package in RStudio.

You’ll also need to connect your Posit account in RStudio, instructions for which are here.

Then, you can click the “Publish” button in the RStudio IDE to deploy your application to your server.

When deploying the app, the app data and all packages used will be bundled and sent to the server. After deployment, you can control who can access the application through the settings on each piece of content.

Handling App Data

When deploying a Shiny application, you can bundle data directly with the app. This process involves including the data files in the same directory as the app itself.

Here’s a general process for bundling data with a Shiny app:

  1. Create a Data Folder: Within your Shiny app project directory, create a separate folder (often named “data”) to store your data files.

  2. Add Data Files: Copy your data files into this folder. These files can be in any format that R can read, such as CSV, RDS, or RData. It’s good practice to use relative paths when referencing these data files in your app.

  3. Refer to Data Files: In your Shiny app code, refer to these data files using relative paths. For example, if you have a file named “mydata.csv” in a folder named “data”, you would load it in R using something like read.csv(“./data/mydata.csv”).

  4. Deploy the App: When you deploy the app to shinyapps.io or Shiny Connect, all files in the app’s directory, including the data files, are bundled and deployed together.

  5. Access the Data: Once deployed, the Shiny app can access the data files as if they were in the local environment because they are part of the deployed app. However, remember that these are read-only. If your app modifies the data, those modifications are lost at the end of the session unless saved to a persistent storage service like a database or cloud storage.

This approach is great for smaller, static datasets. However, if you have large datasets or if the data is updated frequently, it would be better to store the data in a database or a cloud storage service, and have the Shiny app read the data from there.

More Advanced Data Handling

The topics below are beyond the scope of this workshop, but they are useful to know about as you develop more complex Shiny applications.

Sharing and Re-using Data with the pins Package

The pins package is a useful tool for pinning, discovering, and sharing resources. It allows you to cache large datasets on services like Posit Connect or cloud storage services like AWS S3, Google Cloud Storage, Azure, Dropbox, or SharePoint. This can be particularly useful for sharing large datasets or analyses across multiple Shiny applications and keeping said data in-sync.

It can also be used to simply keep important data objects organized locally for your own use, or to share them easily with others.

For more information, you can read the Pins vignettes.

Persistent Data Storage

For apps that require persistent data storage, there are several R packages that can help. The DBI package provides a common interface to communicate with databases from R, and there are various DBI-compliant packages for specific types of databases, like MySQL, RSQLite, and RPostgres.

This application also shows multiple ways to deal with persistent data storage in Shiny, along with code examples of each. The associated article provides more details.

For a tutorial on using databases with Shiny, see this guide from RStudio.

Recap

To summarize, this module covered:

  • Common Gotchas and Debugging Challenges: We discussed common “gotchas” for Shiny beginners, including issues with asynchronous code, reactivity, and complex UIs. We also covered the challenges of debugging Shiny apps.

  • Debugging Shiny Apps: We introduced some useful debugging tools like browser() and reactlog(), and walked through a debugging example.

  • App Deployment: We showed two ways to deploy Shiny apps: shinyapps.io and Posit Connect, and discussed the pros and cons of each. We also talked about how to bundle data with your Shiny app and mentioned ways to more easily share data objects with collaborators or multiple Shiny apps via the pins package.

By the end of this module, participants should have an idea of where to start debugging their Shiny app and some of the unique challenges that debugging in Shiny presents.