Chapter 6 Shiny (BOAST) Specific Coding Practices

One of the most important aspects of Shiny apps is that they are interactive. We want users to interact with the app by changing the values of various inputs and observing what changes. While there are a variety of ways in which we can allow users to manipulate different aspects, there are two important elements to ensure that we (and the users) make the most of Shiny’s reactive environment. We need to plan/design for such interaction and we need to code in a way that makes the most of the interaction.

While planning/designing ostensibly comes before coding, there is a useful back-and-forth that helps use design (and code) better apps. One place where coding can inform planning/designing is understanding how Shiny apps handle user interaction

6.1 Planning Interactions

There are two aspects to every interaction in a Shiny app: there is some triggering event that causes something to happen and there are the results of that trigger event (i.e., the effects). To stick with the function metaphor, we call the triggers inputs and the results, outputs.

6.1.1 Inputs

As you start thinking about your app, you’ll want to thinking about ways in which you want the user to interact with your app. Think beyond the basic interactions of reading text and clicking to a different page. Do you want the user to make choices (more than one at a time?), move a slider, enter a number, enter text, upload a file, click a button, etc.? Each action that you design for a user to take becomes an element in the input object that exists for every app. The input object is a special type of list in R that embraces the reactivity of the Shiny apps.

There are a wide variety of different kinds of inputs that you can plan to use in your apps, including but not limited to

  • Buttons (via bsButton)
  • Checkboxes (via checkboxInput and checkboxGroupInput)
  • Dates (via dateInput and dateRangeInput)
  • Numbers (via numericInput)
  • Radio buttons (via radioButtons and radioGroupButtons)
  • Drop down lists (via selectInput)
  • Sliders (via sliderInput)
  • Text (short text via textInput; large paragraphs via textAreaInput)

With the exception of bsButton, all of the above functions are from the shiny package and create different kinds of inputs. There are additional types and styles of inputs generated by functions from other packages such as shinyWidgets and shinyMatrix that we also use. While all of these functions will create different kinds of inputs, they all share vitally important argument which you must provide a value to: inputId. The inputId argument gives your input a name which allows you to access the user’s entry elsewhere. What is vitally important is that every input must have a unique value for inputId. Missing values for inputId can cause your app to not run at all and duplicated values can cause your app to crash or behave in unexpected ways. Given their importance, we want to be sure that each input’s name (value for inputId) is an informative name.

Since we design the inputs to be something that users interact with, the above input creation functions (*Input) will appear in the UI portion of your app code. To make use of the user’s values in the server portion, you’ll need to call the values using the format input$objName. As an example, if we had a drop-down menu where the user could pick the difficulty level for a game, we might use selectInput(inputId = "difficultyLevel",...) in the UI to create the input object. Then in the server, we would access the user’s choice with input$difficultyLevel. Using informative names will help you keep track of what inputs provide what information. We also recommend keeping a list of input names as part of your design process.

6.1.2 Outputs

The effect or result side of interactivity appears with the notion of outputs in Shiny apps. Just as there is a special list object called input, there’s a matching list object called output. The big question to ask yourself here is what do you want your user to observe as a result of their actions? Outputs generally come in a much smaller set of flavors:

  • Plots
  • Images
  • Tables
  • Text
  • User Interface elements

Outputs require the use of two partner functions: a render* function in the server and a matching *Output function in the UI. Just like with inputs, all output functions have a vital argument called outputId. The value of outputId becomes the name of the object and the way that you can access that object throughout your app. Again, we want informative names.

Later on in this chapter (see below), we’ll look at an example of working with output objects.

6.2 Reactive vs. Observe

As mentioned, the input and output objects are special types of lists in R. They get their special status through the fact that they contain reactive elements. This brings up another area where knowledge of what is happening in your app can help you with design.

Within Shiny apps, there are two classes of objects, reactive and observe, that impart special attributes to objects. In general, a reactive object anticipates changing over time and functioning as an input to other elements of your app. An observe object will call upon reactive elements, perform some task, and return an output. You can think about the *Input functions as creating reactive objects while the render* functions as observe objects.

There are a couple of key differences between reactive and observe:

  1. reactive elements can be used as inputs in other reactive elements as well as observe elements. observe elements cannot be used as inputs to other reactive (or observe) elements.
  2. observe elements are eager–the moment they detect any one of their inputs changing, they update themselves as soon as possible. On the other hand, reactive elements are lazy–while they see when their inputs change, they don’t update until they are called on by another element.

To help highlight this second difference, consider your picking up a cake from a bakery. If you were to act like an observe object, the moment the bakery called to say that the cake was ready, you’d drive to them and pick it up. If you were to act like a reactive element, you’d thank them for the call and make a note. When the party host asks you for the cake, that is when you head to the bakery to pick it up.

You will work with both reactive and observe elements (in particular with values and events) in your apps. While there are various ways in which you can do this, we detail the ways that we use (and want you to copy) them in BOAST.

If you want to learn more about the Shiny’s Reactivity, please check out their Understanding Reactivity page.

6.3 Working with Reactive Values

Reactive values are exactly what you might think: values which react to changes in your app. The only catch is that in order to use them, you must be in an a reactive environment. If you don’t, you’ll see the following error:

Error in .getReactiveEnvironment()$currentContext() : Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive function.)

We will detail how to avoid these errors below.

There are three common ways to work with reactive elements.

6.3.1 input

Anything to which you assign an inputId is automatically created as a reactive element. Whether you’re making a button, a slider, a drop-down menu, etc., your app will store the value of that object which you can then use elsewhere in the code. As mentioned before to call (make use of) any input values you simply type input$fieldName where fieldName is whatever meaningful name you assigned.

The most common cause of the above error is putting input$fieldName in the wrong place. To avoid this error, you must ensure that you are inside an eventReactive or observeEvent call.

6.3.2 reactiveVal

The reactiveVal function allows you to create a single value (much like a variable in R) which will have reactive properties. One key usage of reactiveVal is that it allows you store values which you might want to update while someone uses the app, but not be an input. For example, suppose you want to keep track of a player’s score while they are playing a game. This is a great place to use reactiveVal. We would code this in the following way:

server <- function(input, output, session){
  score <- reactiveVal(0)
  
  observeEvent(
    eventExpr = input$submit,
    handlerExpr = {
      if (correct){
        score(score() + 1)
      } else {
        score(score() - 1)
      }
      output$scoreDisplay <- renderUI({
        paste("Your current score is", score())
      })
    }
  )
}

In the above code, we’ve created score as a reactive element that starts with the initial value of 0. To call score and get its current value we need to use score(). To update score’s value we put the new value (or an expression) as an argument (e.g., score() + 1). Thus, the code score(score() + 1) says “take the current value of score, add 1, and then save as the new value of score”.

Unlike inputs, app users don’t directly change the values of a reactiveVal. Rather there’s an intermediate step. Further, reactiveVal may be called outside fo eventReactive and observeEvent…provided that you are acting on global objects or constants. That is to say, you can’t write submitCounter <- reactiveVal(input$submit) without getting the above error message. In order to use input$fieldName inside of reactiveVal, you must be inside of an eventReactive or observeEvent chunk. However, you can write submitCounter <- reactiveVal(0) and submitCounter(1) anywhere.

6.3.3 reactiveValues

One limitation of reactiveVal is that it is only a single object (one value, one vector, one data frame). This works fine if you just want that. However, let’s say you want to keep track of multiple related values; for example, not only the user’s score, but their number of attempts and how many times they used a hint. You could define three separate reactiveVal objects or you could use a single reactiveValues call. In essence, reactiveValues creates a list of reactiveVals for you.

server <- function(input, output, session){
  playerStats <- reactiveValues(
    score = 0,
    attempts = 0,
    hints = 0
  )
  
  observeEvent(
    eventExpr = input$submit,
    handlerExpr = {
      playerStats$attempts <- playerStats$attempts + 1
      if (correct) {
        playerStats$score <- playerStats$score + 1,
      } else {
        playerStats$score <- playerStats$score - 1
      }
    }
  )
  
  observeEvent(
    eventExpr = input$hint,
    handlerExpr = {
      playerStats$hints <- playerStats$hints + 1
    }
  )
}

A couple things to notice is that unlike reactiveVal you don’t need to append () to the name to get the value and you don’t need to place the new value as an argument. Rather you can use the assignment operator, <-, just as you would in regular R. By using reactiveValues to create a list, playerStats, we can create a clear conceptual link between score, attempts, and hints as well as have them travel together throughout the app. Outside of their nature of being a list of values, reactiveValues still acts like reactiveVal.

6.4 Events

There are two major types of events that we use within the server: eventReactive and observeEvent. Just as you might expect from their names eventReactive is a reactive element and can be stored as an output while observeEvent is an observe element and can’t be stored.

6.4.1 eventReactive

One way to think about eventReactive is that it takes reactiveVal/reactiveValues and goes beyond in a couple of ways. First, you can store more complicated objects such as incorporating logic controls. Second, you define a specific trigger to cause an update (i.e., the eventExpr). Here is an example eventReactive that will switch between various data sets, depending upon the user’s choice (via input$selectedData):

dataSet <- eventReactive(
  eventExpr = input$selectedData,
  valueExpr = {
    switch(
      EXPR = input$selectedData,
      "Iris" = iris,
      "Cars" = mtcars,
      "Penguins" = palmerpenguins::penguins
    )
  },
  ignoreNULL = TRUE,
  ignoreInit = FALSE
)

output$dataTable <- DT::renderDataTable(
  expr = dataSet()
)

In this example, any time the user changes the input field called “selectedData” (i.e., they change which data set they want to look at a.k.a. the “event”), the dataSet object will update. We can then use dataSet() to call the current data set and display that for the user in the output dataTable. Notice by using the eventReactive in this way, we don’t have to write multiple instances of the output$dataTable to handle which data set is currently selected. Thus, eventReactive is a great way to keep our code DRY.

6.4.2 observeEvent

As a contrast, observeEvent allows you to make use of reactive elements and cause things to happen BUT you can’t save/store those things to be used elsewhere. We would use observeEvent if we want to update a plot, text, or other aspect of the UI for the user. As an example suppose that we want to create a box plot based upon the sample size and mean a user sets:

observeEvent(
  eventExpr = c(input$sampleSize, input$mean),
  handlerExpr = {
    localData <- data.frame(
      x = rnorm(
        n = input$sampleSize,
        mean = input$mean,
        sd = 1
      )
    )
    output$dataPlot <- renderPlot(
      normBox <- ggplot(
        data = localData,
        mapping = aes(x = x)
      ) +
        geom_boxplot()
      normBox
    )
  },
  ignoreNULL = TRUE,
  ignoreInit = FALSE
)

Just as in eventReactive, we have the eventExpr argument. This is the trigger(s) that the code looks for changes to. In this case, we’ve tied the observer to two triggers–the sample size and the mean input fields. If the user changes either one of these, the app will immediately run the code that is in the handlerExpr (notice the use of curly braces, {}, to form a code block/chunk). Notice that we create localData and normBox as part of the handlerExpr. Both of these objects only exist inside this particular observeEvent. We cannot access them anywhere else given our current coding. Remember, observeEvent (and other observe elements) do not create stored and accessible objects like reactive elements.

6.4.3 ignoreNULL and ignoreInit

Both eventReactive and observeEvent take the arguments ignoreNULL and ignoreInit. These are two extremely useful arguments to consider as you code your app. Both relate to what you are using as the eventExpr.

When you’re working with input fields or reactive elements, you can run into cases where the input will have the value NULL. Suppose you’ve created a drop down list for a user to select things from and they have yet to make a selection. In this event, the value of input$userSelection could be NULL (unless you’ve provided a default value). By setting ignoreNULL = TRUE, you instruct the eventReactive/observeEvent to not be triggered when eventExpr ends up with a value of NULL. To put this another way, setting ignoreNULL = TRUE will tell your app to wait for the user to do something before carrying out the event while ignoreNULL = FALSE will tell your app to immediately carry out the event.

When you launch a Shiny app two things happen before the user even sees the interface: R reads all of the code and generates the user interface, and R activates (initializes) any eventReactive and observeEvent you’ve created. If you don’t want the app to activate an event on initialization, you need to set ignoreInit = TRUE. If you want the app immediately carry out the event when the app loads, set ignoreInit = FALSE. Leaving ignoreInit = FALSE (the default) does carry some risks. If any code that is part of the valueExpr or the handlrExpr (not just eventExpr) has a value that is either NULL or needs the user to set to something valid, you can run the risk of your app encountering a fatal error and crashing when you try to run it. The following table provides a handy guide to getting the desired behavior out of ignoreNULL and ignoreInit:

Desired Effect Set ignoreNULL to Set ignoreInit to
Run no matter what
(can cause fatal errors)
FALSE FALSE
Run every time except App Initialization FALSE TRUE
Run whenever eventExpr isn’t NULL
(the default)
TRUE FALSE
Run only when the user explicitly does something
(provided eventExpr isn’t NULL)
TRUE TRUE

6.5 Validation

In addition to ignoreNULL and ignoreInit, the shiny package provides an additional tool that you can use in any of render* functions (e.g., renderPlot, renderText, DT::renderDataTable, etc.): validation. Validation will check that any needed inputs are available AND are valid.

The are two components that you’ll need to set up validation are the validate call and at least one need call. You only need one instance of validate inside of each render* function. You will use as many need calls as is necessary to fully set up all of the validation checks you want in place. This will be the first thing that you write inside the expr argument of the render* function. For example, the following code ensure that the sample size set by the user is at least two before generating/displaying the scatter plot.

output$scatterPlot <- renderPlot(
  expr = {
    validate(
      errorClass = "leftParagraphError",
      need(
        expr = input$sampleSize >= 2,
        message = "Sample size must be greater than 2."
      )
    )
    ggplot(
      data = data.frame(
        # Rest of code omitted
      )
    )
  }
)

There is one named argument for validate and that is errorClass. This would be where you put extra text to fully specify which CSS class this error message should belong to. For more details on this, see the section on CSS. In most cases, you can omit this argument thereby just using the default.

Within the need function, you must specify two arguments: expr which is where you will define each validation check and message which is the text you want displayed for the user. For the message you should put something that will help the user fix the error.

6.6 Connecting the User Interface and the Server

If you go back and look at some of the examples in the preceding sections you’ll notice a reoccurring pattern when referencing outputs (e.g., output$scatterPlot, output$dataPlot, output$dataTable):

  • They all start with output$ and immediately followed by a (semi-)descriptive name,
  • They all have the assignment operator, <-, and
  • They all are followed by a function whose name begins with render*

While accessing inputs from the UI is straightforward when you’re in a reactive environment (e.g., input$nameOfInput), working the other direction (i.e., accessing the outputs) takes a bit more work. This is where the render* functions come into play. These functions will take whatever you place in them and prepare those objects so that they be placed into your UI.

In your UI you’ll need to create a placeholder object in your code. For example, you’ll need to have a line of code such as plotOutput("fliperLengthHist"). This creates the space in your app for the render* functions to send the output. For each render* function there is a companion *Output function. Here is an example for displaying a histogram:

freedmanDiaconis <- function(x){ifelse(IQR(x) == 0, 0.1, 2 * IQR(x) / (length(x)^(1/3)))}

ui <- list(
  #..code omitted..
  plotOutput(outputId = "fliperLengthHist")
)

server <- function(input, output, session) {
  #..code omitted..
  output$fliperLengthHist <- renderPlot(
    expr = {
      ggplot(
        data = penguins,
        mapping = aes(x = flipper_length_mm)
      ) +
        geom_histogram(
          binwidth = freedmanDiaconis,
          closed = "left",
          na.rm = TRUE,
          fill = "blue",
          color = "black"
        ) +
        theme_bw() +
        xlab("Flipper length (mm)") +
        scale_y_continuous(expand = expansion(add = c(0, 5)))
    },
    alt = "Penguin flipper lengths appear bimodal with one group centered
    around 190 mm and the second centered around 215 mm"
  )
}

There are a few things to notice in this code example:

  • We needed to include plotOutput in the UI so that R/Shiny knows where to send the histogram.
  • Since we are creating a static plot (i.e., the histogram doesn’t change), we did not wrap the renderPlot inside of an observeEvent.
  • Within the renderPlot we have two REQUIRED elements:
    • The expr is where you’ll write the code to make the histogram
    • The alt provides alt text for your plot.
    • Not all render* functions will have the alt argument; renderPlot does and you are required to fill this argument in with meaningful text; see Section 13.9 for more information.

This second required element, the alt argument, is how you apply alternative text (“alt text”) to a plot. This text should be descriptive for what appears in plot as this is what screen readers will read out loud to low-/no- vision users. You must have alt text on all visualizations.

You might have also noticed some of the coding that is part of the ggplot calls such as the theme_bw and scale_y_continuous. Check out Chapter 13 for more information.

6.6.1 Table of render* and *Output Companions

The following table contains the main pairings of functions that you’ll need to use, from most to least common:

Type of Object In the UI In the Server
Plots plotOutput
*alt required
renderPlot
Tables
Requires the DT package
DT::dataTableOutput DT::renderDataTable
Grading Icons
Requires the boastUtils package
uiOutput boastUtils::renderIcon
User Interface Elements uiOutput renderUI
Text textOutput renderText
Raw Output verbatimTextOutput renderPrint

There is some flexibility between the last three items that will depend upon what exactly you’re trying to do. We’ll handle those on a case-by-case basis. You may see htmlOutput instead of uiOutput; we would like to use uiOutput as your primary choice.