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
andcheckboxGroupInput
) - Dates (via
dateInput
anddateRangeInput
) - Numbers (via
numericInput
) - Radio buttons (via
radioButtons
andradioGroupButtons
) - Drop down lists (via
selectInput
) - Sliders (via
sliderInput
) - Text (short text via
textInput
; large paragraphs viatextAreaInput
)
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
:
reactive
elements can be used as inputs in otherreactive
elements as well asobserve
elements.observe
elements cannot be used as inputs to otherreactive
(orobserve
) elements.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 anobserveEvent
. - Within the
renderPlot
we have two REQUIRED elements:
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.