library(tidyverse)
library(shiny)
Shiny is a package from RStudio that can be used to build interactive web pages with R. While that may sound scary because of the words “web pages”, it’s geared to R users who have no experience with web development, and you do not need to know any HTML/CSS/JavaScript.
You can do quite a lot with Shiny: think of it as an easy way to make an interactive web page, and that web page can seamlessly interact with R and display R objects (plots, tables, of anything else you do in R). To get a sense of the wide range of things you can do with Shiny, you can visit the Shiny gallery, which hosts examples of basic (and complex) Shiny apps.
In this activity, we’ll walk through all the steps of building a Shiny app using a dataset that lets you explore the products available at the Virginia ABC stores.1 The final version of the app can be seen here. Any activity deemed as an exercise throughout this tutorial is not mandatory for building our app, but they are good for getting more practice with Shiny.
If you want even more practice, another great tutorial is the official Shiny tutorial. RStudio also provides a handy cheatsheet to remember all the little details after you already learned the basics.
You’ll need to have the shiny
package, so install it.
install.packages("shiny")
To ensure you successfully installed Shiny, try running one of the demo apps.
library(shiny)
runExample("01_hello")
If the example app is running, press Escape to close the app, and you are ready to build your first Shiny app!
Every Shiny app is composed of a two parts: a web page that shows the app to the user, and a computer that powers the app. The computer that runs the app can either be your own laptop (such as when you’re running an app from RStudio) or a server somewhere else. You, as the Shiny app developer, need to write these two parts (you’re not going to write a computer, but rather the code that powers the app). In Shiny terminology, they are called UI (user interface) and server.
UI is just a web document that the user gets to see, it’s HTML that you write using Shiny’s functions. The UI is responsible for creating the layout of the app and telling Shiny exactly where things go. The server is responsible for the logic of the app; it’s the set of instructions that tell the web page what to show when the user interacts with the page.
If you look at the app we will be building, the page that you see is built with the UI code. You’ll notice there are some controls that you, as the user, can manipulate. If you adjust the price or choose a type of alcohol, you’ll notice that the plot and the table get updated. The UI is responsible for creating these controls and telling Shiny where to place the controls and where to place the plot and table, while the server is responsible for creating the actual plot or the data in the table.
All Shiny apps follow the same template:
library(shiny)
ui <- fluidPage()
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
This template is by itself a working minimal Shiny app that doesn’t do much. It initializes an empty UI and an empty server, and runs an app using these empty parts. Copy this template into a new file named app.R
in a new folder. It is very important that the name of the file is app.R
, otherwise it would not be recognized as a Shiny app. It is also very important that you place this app in its own folder, and not in a folder that already has other R scripts or files, unless those other files are used by your app.
After saving the file, RStudio should recognize that this is a Shiny app, and you should see the usual Run button at the top change to Run App.
If you don’t see the Run App button, it means you either have a very old version of RStudio, don’t have Shiny installed, or didn’t follow the file naming conventions.
Click the Run App button, and now your app should run. You won’t see much because it’s an empty app, but you should see that the console has some text printed in the form of Listening on http://127.0.0.1:5274
and that a little stop sign appeared at the top of the console. You’ll also notice that you can’t run any commands in the console. This is because R is busy–your R session is currently powering a Shiny app and listening for user interaction (which won’t happen because the app has nothing in it yet).
Click the stop button to stop the app, or press the Escape key.
You may have noticed that when you click the Run App button, all it’s doing is just running the function shiny::runApp()
in the console. You can run that command instead of clicking the button if you prefer.
Exercise: Try running the empty app using the runApp()
function instead of using the Run App button.
Another way to define a Shiny app is by separating the UI and server code into two files: ui.R
and server.R
. This is the preferable way to write Shiny apps when the app is complex and involves more code, but in this tutorial we’ll stick to the simple single file. If you want to break up your app into these two files, you simply put all code that is assigned to the ui
variable in ui.R
and all the code assigned to the server
function in server.R
. When RStudio sees these two files in the same folder, it will know you’re writing a Shiny app.
Exercise: Try making a new Shiny app by creating the two files ui.R
and server.R
. Remember that they have to be in the same folder. Also remember to put them in a new, isolated folder (not where your app.R
already exists).
You can also create a new Shiny app using RStudio’s menu by selecting File > New File > Shiny Web App…. If you do this, RStudio will let you choose if you want a single-file app (app.R
) or a two-file app (ui.R
+server.R
). RStudio will initialize a simple functional Shiny app with some code in it. I personally don’t use this feature because I find it easier to simply type the few lines of a Shiny app and save the files.
The dataset we’ll be using contains information about all the products sold by the Virginia ABC stores and was previously scraped by taber. The processed dataset we’ll be using in this app is available here. Download it now and place this file in the same folder as your Shiny app. Make sure the file is named abc.csv
.
Add a line in your app to load the data into a variable called abc
. It should look something like this (be sure to to add library(tidyverse)
or library(readr)
to the script so you can use the read_csv
function):
abc <- read_csv("abc.csv")
Place this line in your app as the second line, just after library(shiny)
and library(tidyverse)
. Make sure the file path and file name are correct, otherwise your app won’t run. Try to run the app to make sure the file can be loaded without errors.
If you want to verify that the app can successfully read the data, you can add a print()
statement after reading the data. This won’t make anything happen in your Shiny app, but you will see a summary of the dataset printed in the console, which should let you know that the dataset was indeed loaded correctly. You can place the following line after reading the data:
print(str(abc))
Once you get confirmation that the data is properly loaded, you can remove that line.
Exercise: Load the data file into R and get a feel for what’s in it. How big is it, what variables are there, what are the normal price ranges, etc.
Let’s start populating our app with some elements visually. This is usually the first thing you do when writing a Shiny app - add elements to the UI.
You can place R strings inside fluidPage()
to render text.
fluidPage("Virginia ABC Stores", "prices")
Replace the line in your app that assigns an empty fluidPage()
into ui
with the one above, and run the app.
The entire UI will be built by passing comma-separated arguments into the fluidPage()
function. By passing regular text, the web page will just render boring unformatted text.
Exercise: Add several more strings to fluidPage()
and run the app. Nothing too exciting is happening yet, but you should just see all the text appear in one contiguous block.
If we want our text to be formatted nicer, Shiny has many functions that are wrappers around HTML tags that format text. We can use the h1()
function for a top-level header (<h1>
in HTML), h2()
for a secondary header (<h2>
in HTML), strong()
to make text bold (<strong>
in HTML), em()
to make text italicized (<em>
in HTML), and many more.
There are also functions that are wrappers to other HTML tags, such as br()
for a line break, img()
for an image, a()
for a hyperlink, and others.
All of these functions are actually just wrappers to HTML tags with the equivalent name. You can add any arbitrary HTML tag using the tags
object, which you can learn more about by reading the help file on tags
.
Just as a demonstration, try replacing the fluidPage()
function in your UI with
ui <- fluidPage(
h1("My app"),
"Virginia",
"ABC",
br(),
"Store",
strong("prices")
)
Run the app with this code as the UI. Notice the formatting of the text and understand why it is rendered that way.
For people who know basic HTML: any named argument you pass to an HTML function becomes an attribute of the HTML element, and any unnamed argument will be a child of the element. That means that you can, for example, create blue text with
div("this is blue", style = "color: blue;")
.
Exercise: Experiment with different HTML-wrapper functions inside fluidPage()
. Run the fluidPage(...)
function in the console and see the HTML that it creates.
We could add a title to the app with h1()
, but Shiny also has a special function titlePanel()
. Using titlePanel()
not only adds a visible big title-like text to the top of the page, but it also sets the “official” title of the web page. This means that when you look at the name of the tab in the browser, you’ll see this title.
Overwrite the fluidPage()
that you experimented with so far, and replace it with the simple one below, that simply has a title and nothing else.
fluidPage(
titlePanel("Virginia ABC Store Prices")
)
Exercise: Look at the documentation for the titlePanel()
function and notice it has another argument. Use that argument and see if you can see what it does.
You may have noticed that so far, by just adding text and HTML tags, everything is unstructured and the elements simply stack up one below the other in one column. We’ll use sidebarLayout()
to add a simple structure. It provides a simple two-column layout with a smaller sidebar and a larger main panel. We’ll build our app such that all the inputs that the user can manipulate will be in the sidebar, and the results will be shown in the main panel on the right.
Add the following code after the titlePanel()
sidebarLayout(
sidebarPanel("our inputs will go here"),
mainPanel("the results will go here")
)
Remember that all the arguments inside fluidPage()
need to be separated by commas.
So far our complete app looks like this (hopefully this isn’t a surprise to you)
library(shiny)
library(tidyverse)
abc <- read_csv("abc.csv")
ui <- fluidPage(
titlePanel("Virginia ABC Store prices"),
sidebarLayout(
sidebarPanel("our inputs will go here"),
mainPanel("the results will go here")
)
)
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
If you want to be a lot more flexible with the design, you can have much more fine control over where things go by using a grid layout. We won’t cover that here, but if you’re interested, look at the documentation for
?column
and?fluidRow
.
Exercise: Add some UI into each of the two panels (sidebar panel and main panel) and see how your app now has two columns.
This was already mentioned, but it’s important to remember: the entire UI is just HTML, and Shiny simply gives you easy tools to write it without having to know HTML. To convince yourself of this, look at the output when printing the contents of the ui
variable.
print(ui)
<div class="container-fluid">
<h2>Virginia ABC Store prices</h2>
<div class="row">
<div class="col-sm-4">
<form class="well">our inputs will go here</form>
</div>
<div class="col-sm-8">the results will go here</div>
</div>
</div>
This should make you appreciate Shiny for not making you write horrendous HTML by hand.
Inputs are what gives users a way to interact with a Shiny app. Shiny provides many input functions to support many kinds of interactions that the user could have with an app. For example, textInput()
is used to let the user enter text, numericInput()
lets the user select a number, dateInput()
is for selecting a date, selectInput()
is for creating a select box (aka a dropdown menu).
All input functions have the same first two arguments: inputId
and label
. The inputId
will be the name that Shiny will use to refer to this input when you want to retrieve its current value. It is important to note that every input must have a unique inputId
. If you give more than one input the same id, Shiny will unfortunately not give you an explicit error, but your app won’t work correctly. The label
argument specifies the text in the display label that goes along with the input widget. Every input can also have multiple other arguments specific to that input type. The only way to find out what arguments you can use with a specific input function is to look at its help file.
Exercise: Read the documentation of ?numericInput
and try adding a numeric input to the UI. Experiment with the different arguments. Run the app and see how you can interact with this input. Then try different inputs types.
The first input we want to have is for specifying a price range (minimum and maximum price). The most sensible types of input for this are either numericInput()
or sliderInput()
since they are both used for selecting numbers. If we use numericInput()
, we’d have to use two inputs, one for the minimum value and one for the maximum. Looking at the documentation for sliderInput()
, you’ll see that by supplying a vector of length two as the value
argument, it can be used to specify a range rather than a single number. This sounds like what we want in this case, so we’ll use sliderInput()
.
To create a slider input, a maximum value needs to be provided. We could use the maximum price in the dataset, which is $5,002, but I doubt I’d ever buy something that expensive. I think $100 is a more reasonable max price for me, and about 94% of the products in this dataset are below $100, so let’s use that as our max.
By looking at the documentation for the slider input function, the following piece of code can be constructed.
sliderInput("priceInput", "Price", min = 0, max = 100,
value = c(25, 40), pre = "$")
Place the code for the slider input inside sidebarPanel()
(replace the text we wrote earlier with this input).
Exercise: Run the code of the sliderInput()
in the R console and see what it returns. Change some of the parameters of sliderInput()
, and see how that changes the result. It’s important to truly understand that all these functions in the UI are simply a convenient way to write HTML, as is apparent whenever you run these functions on their own.
Usually when going to the liquor store you know what kind of alcohol you want, and you don’t want to waste your time in the wrong section. The same is true in our app, we should be able to choose what type of product we want.
For this we want some kind of a text input. But allowing the user to enter text freely isn’t the right solution because we want to restrict the user to only a few choices. We could either use radio buttons or a select box for our purpose. Let’s use radio buttons for now since there are only a few options, so take a look at the documentation for radioButtons()
and come up with a reasonable input function code. It should look like this:
radioButtons("typeInput", "Product type",
choices = c("Mixers", "Rimmers", "Spirits", "Wine"),
selected = "Spirits")
Add this input code inside sidebarPanel()
, after the previous input (separate them with a comma).
If you look at that input function and think “what if there were 100 types, listing them by hand would not be fun, there’s got to be a better way!”, then you’re right. This is where
uiOutput()
comes in handy, but we’ll talk about that later.
I should probably decide how drunk I want to get by selecting the proof of the alcohol. We could use a sliderInput()
since proof is numeric, but I’ve also simplified it by creating a categorical variable by binning the products’ proofs. The most appropriate input type in this case is probably the select box. Look at the documentation for selectInput()
and create an input function. Let’s have 0-40, 40-80, 80-120, 120-160, and 160+ as the options.
selectInput("proofInput", "Proof",
choices = c("0-40", "40-80", "80-120", "120-160", "160+"))
Add this function as well to your app. If you followed along, your entire app should have this code:
library(shiny)
library(tidyverse)
abc <- read_csv("abc.csv")
ui <- fluidPage(
titlePanel("Virginia ABC Store prices"),
sidebarLayout(
sidebarPanel(
sliderInput("priceInput", "Price", min = 0, max = 100,
value = c(25, 40), pre = "$"),
radioButtons("typeInput", "Product type",
choices = c("Mixers", "Rimmers", "Spirits", "Wine"),
selected = "Spirits"),
selectInput("proofInput", "Proof",
choices = c("0-40", "40-80", "80-120", "120-160", "160+"))
),
mainPanel("the results will go here")
)
)
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
After creating all the inputs, we should add elements to the UI to display the outputs. Outputs can be any object that R creates and that we want to display in our app - such as a plot, a table, or text. We’re still only building the UI, so at this point we can only add placeholders for the outputs that will determine where an output will be and what its ID is, but it won’t actually show anything. Each output needs to be constructed in the server code later.
Shiny provides several output functions, one for each type of output. Similarly to the input functions, all the output functions have an outputId
argument that is used to identify each output, and this argument must be unique for each output.
At the top of the main panel we’ll have a plot showing some visualization of the results. Since we want a plot, the function we use is plotOutput()
.
Add the following code into the mainPanel()
(replace the existing text):
plotOutput("coolplot")
This will add a placeholder in the UI for a plot named coolplot.
Exercise: To remind yourself that we are still merely constructing HTML and not creating actual plots yet, run the above plotOutput()
function in the console to see that all it does is create some HTML.
Below the plot, we will have a table that shows all the results. To get a table, we use the tableOutput()
function.
Here is a simple way to create a UI element that will hold a table output:
tableOutput("results")
Add this output to the mainPanel()
as well. Maybe add a couple br()
in between the two outputs, just as a space buffer so that they aren’t too close to each other.
If you’ve followed along, your app should now have this code:
library(shiny)
library(tidyverse)
abc <- read_csv("abc.csv")
ui <- fluidPage(
titlePanel("Virginia ABC Store prices"),
sidebarLayout(
sidebarPanel(
sliderInput("priceInput", "Price", min = 0, max = 100,
value = c(25, 40), pre = "$"),
radioButtons("typeInput", "Product type",
choices = c("Mixers", "Rimmers", "Spirits", "Wine"),
selected = "Spirits"),
selectInput("proofInput", "Proof",
choices = c("0-40", "40-80", "80-120", "120-160", "160+"))
),
mainPanel(
plotOutput("coolplot"),
tableOutput("results")
)
)
)
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
So far we only wrote code inside that was assigned to the ui
variable (or code that was written in ui.R
). That’s usually the easier part of a Shiny app. Now we have to write the server
function, which will be responsible for listening to changes to the inputs and creating outputs to show in the app.
If you look at the server function, you’ll notice that it is always defined with two arguments: input
and output
. You must define these two arguments! Both input
and output
are list-like objects. As the names suggest, input
is a list you will read values from and output
is a list you will write values to. input
will contain the values of all the different inputs at any given time, and output
is where you will save output objects (such as tables and plots) to display in your app.
Recall that we created two output placeholders: coolplot (a plot) and results (a table). We need to write code in R that will tell Shiny what kind of plot or table to display. There are three rules to build an output in Shiny.
output
list (remember the app template - every server function has an output
argument)render*
function, where *
is the type of outputinput
list (every server function has an input
argument)The third rule is only required if you want your output to depend on some input, so let’s first see how to build a very basic output using only the first two rules. We’ll create a plot and send it to the coolplot output.
output$coolplot <- renderPlot({
plot(rnorm(100))
})
This simple code shows the first two rules: we’re creating a plot inside the renderPlot()
function, and assigning it to coolplot in the output
list. Remember that every output created in the UI must have a unique ID, now we see why. In order to attach an R object to an output with ID x, we assign the R object to output$x
.
Since coolplot was defined as a plotOutput
, we must use the renderPlot
function, and we must create a plot inside the renderPlot
function.
If you add the code above inside the server function, you should see a plot with 100 random points in the app.
Exercise: The code inside renderPlot()
doesn’t have to be only one line, it can be as long as you’d like as long as it returns a plot. Try making a more complex plot using ggplot2
. The plot doesn’t have to use our dataset, it could be anything, just to make sure you can use renderPlot()
.
Now we’ll take the plot one step further. Instead of always plotting the same plot (100 random numbers), let’s use the minimum price selected as the number of points to show. It doesn’t make too much sense, but it’s just to learn how to make an output depend on an input.
output$coolplot <- renderPlot({
plot(rnorm(input$priceInput[1]))
})
Replace the previous code in your server function with this code, and run the app. Whenever you choose a new minimum price range, the plot will update with a new number of points. Notice that the only thing different in the code is that instead of using the number 100
we are using input$priceInput[1]
.
What does this mean? Just like the variable output
contains a list of all the outputs (and we need to assign code into them), the variable input
contains a list of all the inputs that are defined in the UI. input$priceInput
return a vector of length 2 containing the minimum and maximum price. Whenever the user manipulates the slider in the app, these values are updated, and whatever code relies on it gets re-evaluated. This is a concept known as reactivity, which we will get to in a few minutes.
Notice that these short 3 lines of code are using all the 3 rules for building outputs: we are saving to the output
list (output$coolplot <-
), we are using a render*
function to build the output (renderPlot({})
), and we are accessing an input value (input$priceInput[1]
).
Now we have all the knowledge required to build a plot visualizing some aspect of the data. We’ll create a simple histogram of the alcohol content of the products by using the same 3 rules to create a plot output.
First we need to make sure ggplot2
is loaded, so add a library(ggplot2)
at the top (or just continue to use library(tidyverse)
.
Next we’ll return a histogram of bottle size Size
from renderPlot()
. Let’s start with just a histogram of the whole data, unfiltered.
output$coolplot <- renderPlot({
ggplot(abc, aes(Size)) +
geom_histogram()
})
If you run the app with this code inside your server, you should see a histogram in the app. But if you change the input values, nothing happens yet, so the next step is to actually filter the dataset based on the inputs.
Recall that we have 3 inputs: priceInput
, typeInput
, and proofInput
. We can filter the data based on the values of these three inputs. We’ll use dplyr
functions to filter the data, so be sure to include dplyr
at the top. Then we’ll plot the filtered data instead of the original data.
output$coolplot <- renderPlot({
filtered <- abc %>%
filter(CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput
)
ggplot(filtered, aes(Size)) +
geom_histogram()
})
Place this code in your server function and run the app. If you change any input, you should see the histogram update.
Read this code and understand it. You’ve successfully created an interactive app - the plot is changing according to the user’s selection.
To make sure we’re on the same page, here is what your code should look like at this point:
library(shiny)
library(tidyverse)
abc <- read_csv("abc.csv")
ui <- fluidPage(
titlePanel("Virginia ABC Store prices"),
sidebarLayout(
sidebarPanel(
sliderInput("priceInput", "Price", min = 0, max = 100,
value = c(25, 40), pre = "$"),
radioButtons("typeInput", "Product type",
choices = c("Mixers", "Rimmers", "Spirits", "Wine"),
selected = "Spirits"),
selectInput("proofInput", "Proof",
choices = c("0-40", "40-80", "80-120", "120-160", "160+"))
),
mainPanel(
plotOutput("coolplot"),
tableOutput("results")
)
)
)
server <- function(input, output) {
output$coolplot <- renderPlot({
filtered <- abc %>%
filter(CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput
)
ggplot(filtered, aes(Size)) +
geom_histogram()
})
}
shinyApp(ui = ui, server = server)
Exercise: The current plot doesn’t look very nice, you could enhance the plot and make it much more pleasant to look at.
Building the next output should be much easier now that we’ve done it once. The other output we have was called results
(as defined in the UI) and should be a table of all the products that match the filters. Since it’s a table output, we should use the renderTable()
function. We’ll do the exact same filtering on the data, and then simply return the data as a data.frame. Shiny will know that it needs to display it as a table because it’s defined as a tableOutput
.
The code for creating the table output should make sense to you without too much explanation:
output$results <- renderTable({
filtered <- abc %>%
filter(CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput
)
filtered
})
Add this code to your server. Don’t overwrite the previous definition of output$coolplot
, just add this code before or after that, but inside the server function. Run your app, and be amazed! You can now see a table showing all the products at the Virginia ABC store that match your criteria.
Notice that in building
ui
, we are using a predefined function calledfluidPage()
so all of our different elements for the UI are separated by commas. This is because each element for the page is an argument for thefluidPage()
function. In buildingserver
, we are writing a new function. For this reason we don’t have to separate each element with a comma. We just write it like we would any other function!
Exercise: Add a new output. Either a new plot, a new table, or some piece of text that changes based on the inputs. For example, you could add a text output (textOutput()
in the UI, renderText()
in the server) that says how many results were found. If you choose to do this, I recommend first adding the output to the UI, then building the output in the server with static text to make sure you have the syntax correct. Only once you can see the text output in your app you should make it reflect the inputs. Pro-tip: since textOutput()
is written in the UI, you can wrap it in other UI functions. For example, h2(textOutput(...))
will result in larger text.
Shiny uses a concept called reactive programming. This is what enables your outputs to react to changes in inputs. Reactivity in Shiny is complex, but as an extreme oversimplification, it means that when the value of a variable x
changes, then anything that relies on x
gets re-evaluated. Notice how this is very different from what you are used to in R. Consider the following code:
x <- 5
y <- x + 1
x <- 10
What is the value of y
? It’s 6. But in reactive programming, if x
and y
are reactive variables, then the value of y
would be 11 because it would be updated whenever x
is changed. This is a very powerful technique that is very useful for creating the responsiveness of Shiny apps, but it might be a bit weird at first because it’s a very different concept from what you’re used to.
Only reactive variables behave this way, and in Shiny all inputs are automatically reactive. That’s why you can always use input$x
in render functions, and you can be sure that whatever output depends on x
will use the updated value of x
whenever x
changes.
You might be wondering what it means to “depend” on a variable. This is not the official terminology, but it simply means that the variable is referenced in the code. So by merely accessing the value of a reactive variable, it causes the current code block to “depend” on that variable. Consider the following sample code to create a plot with a specific number of points in a specific color:
output$someoutput <- renderPlot({
col <- input$mycolor
num <- input$mynumber
plot(rnorm(num), col = col)
})
The above render function accesses two different inputs: input$mycolor
and input$mynumber
. This means that this code block depends on both of these variables, so whenever either one of the two inputs is updated, the code gets re-executed with the new input values and output$someoutput
is updated.
One very important thing to remember about reactive variables (such as the input
list) is that they can only be used inside reactive contexts. Any render*
function is a reactive context, so you can always use input$x
or any other reactive variable inside render functions. There are two other common reactive contexts that we’ll get to in a minute: reactive({})
and observe({})
. To show you what this means, let’s try accessing the price input value in the server function, without explicitly being inside a reactive context. Simply add print(input$priceInput)
inside the server
function, and you will get an error when running the app:
Operation not allowed without an active reactive context. (You tried to do something that can only be done from inside a reactive expression or observer.)
Shiny is very clear about what the error is: we are trying to access a reactive variable outside of a reactive context. To fix this, we can use the observe({})
function to access the input
variable. Inside the server, replace print(input$priceInput)
with observe({ print(input$priceInput) })
, and now the app should run fine. Note that this observe({})
statement depends on input$priceInput
, so whenever you change the value of the price, the code inside this observe({})
will run again, and the new value will be printed. This is actually a very simple yet useful debugging technique in Shiny: often you want to know what value a reactive variable holds, so you need to remember to wrap the cat(input$x)
or print(input$x)
by an observe({})
.
So far we only saw one reactive variable: the input
list. You can also create your own reactive variables using the reactive({})
function. The reactive({})
function is similar to observe({})
in that it is also a reactive context, which means that it will get re-run whenever any of the reactive variables in it get updated. The difference between them is that reactive({})
returns a value. To see it in action, let’s create a variable called priceDiff
that will be the difference between the maximum and minimum price selected. If you try to naively define priceDiff <- diff(input$priceInput)
, you’ll see the same error as before about doing something outside a reactive context. This is because input$priceInput
is a reactive variable, and we can’t use a reactive variable outside a reactive context. Since we want to assign a value, we use the reactive({})
function. Try adding the following line to your server:
priceDiff <- reactive({
diff(input$priceInput)
})
Now your app will run. If you want to access a reactive variable defined with reactive({})
, you must add parentheses after the variable name, as if it’s a function. To demonstrate this, add observe({ print(priceDiff()) })
to your server function. Notice that we use priceDiff()
rather than priceDiff
. It’s very important to remember this, because you can get confusing unclear errors if you simply try to access a custom reactive variable without the parentheses.
You can think of reactivity as causing a chain reaction: when one reactive value changes, anything that depends on it will get updated. If any of the updated values are themselves reactive variables, then any reactive contexts that depend on those variables will also get updated in turn. As a concrete example, let’s think about what happens when you change the value of the priceInput
on the page. Since input$priceInput
is a reactive variable, any expression that uses it will get updated. This means the two render functions from earlier will execute because they both depend on input$priceInput
, as well as the priceDiff
variable because it also depends on it. But since priceDiff
is itself a reactive variable, Shiny will check if there is anything that depends on priceDiff
, and indeed there is - the observe({})
function that prints the value of priceDiff
. So once priceDiff
gets updated, the observe({})
function will run, and the value will get printed.
Reactivity is usually the hardest part about Shiny to understand, so if you don’t quite get it, don’t feel bad. Try reading this section again, and I promise that with time and experience you will get more comfortable with reactivity. Once you do feel more confident with reactivity, it may be a good idea to read more advanced documentation describing reactivity, since this section greatly simplifies ideas to make them more understandable. A great resource is RStudio’s tutorial on reactivity.
Before continuing to the next section, you can remove all the observe({})
and reactive({})
functions we wrote in this section since they were all just for learning purposes.
Exercise: Read this section again and really understand what a reactive variable means, what the 3 main reactive contexts are, how you can define reactive variables, and how a reactivity chain of events works.
You may have noticed that we have the exact same code filtering the dataset in two places, once in each render function. We can solve that problem by defining a reactive variable that will hold the filtered dataset, and use that variable in the render functions.
The first step would be to create the reactive variable. The following code should be added to the server function.
filtered <- reactive({
abc %>%
filter(CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput
)
})
The variable filtered
is being defined exactly like before, except the body is wrapped by a reactive({})
, and it’s defined in the server function instead of inside the individual render functions. Now that we have our reactive variable, we can use it in the output render functions. Try it yourself, and when you think you’re done, check the code below. Don’t forget that in order to access the value of a reactive expression, you must follow the name of the variable with parentheses! This is how your server function should look like now.
server <- function(input, output) {
filtered <- reactive({
abc %>%
filter(
CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput
)
})
output$coolplot <- renderPlot({
ggplot(filtered(), aes(Size)) +
geom_histogram()
})
output$results <- renderTable({
filtered()
})
}
As a reminder, Shiny creates a dependency tree with all the reactive expressions to know what value depends on what other value. For example, when the price input changes, Shiny looks at what values depend on price, and sees that filtered
is a reactive expression that depends on the price input, so it re-evaluates filtered
. Then, because filtered
is changed, Shiny now looks to see what expressions depend on filtered
, and it finds that the two render functions use filtered
. So Shiny re-executes the two render functions as well.
One of the output functions you can add in the UI is uiOutput()
. According to the naming convention (e.g.. plotOutput()
is an output to render a plot), this is an output used to render more UI. This may sound a bit confusing, but it’s actually very useful. It’s usually used to create inputs (or any other UI) from the server, or in other words - you can create inputs dynamically.
Any input that you normally create in the UI is created when the app starts, and it cannot be changed. But what if one of your inputs depends on another input? In that case, you want to be able to create an input dynamically, in the server, and you would use uiOutput()
. uiOutput()
can be used to create any UI element, but it’s most often used to create input UI elements. The same rules regarding building outputs apply, which means the output (which is a UI element in this case) is created with the function renderUI()
.
As a very basic example, consider this app:
library(shiny)
ui <- fluidPage(
numericInput("num", "Maximum slider value", 5),
uiOutput("slider")
)
server <- function(input, output) {
output$slider <- renderUI({
sliderInput("slider", "Slider", min = 0,
max = input$num, value = 0)
})
}
shinyApp(ui = ui, server = server)
If you run that tiny app, you will see that whenever you change the value of the numeric input, the slider input is re-generated. This behavior can come in handy often.
We can use this concept in our app to populate the choices for the sub-type of alcohol selector. The type selector currently only holds 4 values that we manually entered, but there are a vast number of sub-types of alcohol we could include (e.g. brandy, cordials, whiskey). Instead we could render the sub-type selector in the server and use the data to determine what categories it can have.
First we need to add the selectInput("subtypeInput", ...)
in the UI with
uiOutput("subtypeInput")
Then we need to create the output (which will create a UI element - yeah, it can be a bit confusing at first), so add the following code to the server function:
output$subtypeInput <- renderUI({
selectInput("subtypeInput", "Subtype",
sort(unique(abc$Subtype)),
selected = "Whiskey")
})
Finally to make sure the data properly updates, change filtered
in the server
function to:
filtered <- reactive({
abc %>%
filter(
CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput,
Subtype == input$subtypeInput
)
})
Now if you run the app, you should be able to see the different sub-types of alcohol available in the Virginia ABC stores.
You might notice that when you first run the app, each of the two outputs are throwing an error message, but the error message goes away after a second. The problem is that when the app initializes, filtered
is trying to access the subtype input, but the subtype input hasn’t been created yet. After Shiny finishes loading fully and the subtype input is generated, filtered
tries accessing it again, this time it’s successful, and the error goes away.
Once we understand why the error is happening, fixing it is simple. Inside the filtered
reactive function, we should check if the sub-type input exists, and if not then just return NULL
.
filtered <- reactive({
if(is.null(input$subtypeInput)) {
return(NULL)
}
abc %>%
filter(
CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput,
Subtype == input$subtypeInput
)
})
Now when the render function tries to access the data, they will get a NULL
value before the app is fully loaded. You will still get an error, because the ggplot
function will not work with a NULL
dataset, so we also need to make a similar check in the renderPlot()
function. Only once the data is loaded, we can try to plot.
output$coolplot <- renderPlot({
if (is.null(filtered())) {
return()
}
ggplot(filtered(), aes(Size)) +
geom_histogram()
})
The renderTable()
function doesn’t need this fix applied because Shiny doesn’t have a problem rendering a NULL
table.
Exercise: Change the product type radio buttons to get generated in the server with the values from the dataset, instead of being created in the UI with the values entered manually. If you’re feeling confident, try adding an input for “sub-type” that will get re-generated every time a new type is chosen, and will be populated with all the sub-type options available for the currently selected type (for example, if WINE is selected, then the sub-type are white wine, red wine, etc.).
In case you got lost somewhere, here is the final code. The app is now functional, but there are plenty of features you can add to make it better.
library(shiny)
library(tidyverse)
abc <- read_csv("abc.csv")
ui <- fluidPage(titlePanel("Virginia ABC Store prices"),
sidebarLayout(
sidebarPanel(
sliderInput(
"priceInput",
"Price",
min = 0,
max = 100,
value = c(25, 40),
pre = "$"
),
radioButtons(
"typeInput",
"Product type",
choices = c("Mixers", "Rimmers", "Spirits", "Wine"),
selected = "Spirits"
),
selectInput(
"proofInput",
"Proof",
choices = c("0-40", "40-80", "80-120", "120-160", "160+")
),
uiOutput("subtypeInput")
),
mainPanel(plotOutput("coolplot"),
tableOutput("results"))
))
server <- function(input, output) {
filtered <- reactive({
if(is.null(input$subtypeInput)) {
return(NULL)
}
abc %>%
filter(
CurrentPrice >= input$priceInput[1],
CurrentPrice <= input$priceInput[2],
Type == input$typeInput,
ProofBin == input$proofInput,
Subtype == input$subtypeInput
)
})
output$subtypeInput <- renderUI({
selectInput("subtypeInput", "Subtype",
sort(unique(abc$Subtype)),
selected = "Whiskey")
})
output$coolplot <- renderPlot({
if (is.null(filtered())) {
return()
}
ggplot(filtered(), aes(Size)) +
geom_histogram()
})
output$results <- renderTable({
filtered()
})
}
shinyApp(ui = ui, server = server)
Shiny is extremely powerful and has lots of features that we haven’t covered. Here’s a sneak peek of just a few other common Shiny features that are not too advanced.
You can include Shiny inputs and outputs in an Rmarkdown document! This means that your Rmarkdown document can be interactive. Learn more here. Here’s a simple example of how to include interactive Shiny elements in an Rmarkdown.
---
output: html_document
runtime: shiny
---
```{r echo=FALSE, eval = TRUE} #delete this comment
sliderInput("num", "Choose a number",
0, 100, 20)
renderPlot({
plot(seq(input$num))
})
```
You can use conditionalPanel()
to either show or hide a UI element based on a simple condition, such as the value of another input. Learn more with ?conditionalPanel
.
library(shiny)
ui <- fluidPage(
numericInput("num", "Number", 5, 1, 10),
conditionalPanel(
"input.num >=5",
"Hello!"
)
)
server <- function(input, output) {}
shinyApp(ui = ui, server = server)
Whenever you use tableOutput()
+ renderTable()
, the table that Shiny creates is a static and boring-looking table. If you download the DT
package, you can replace the default table with a much sleeker table by just using DT::dataTableOutput()
+ DT::renderDataTable()
. It’s worth trying. Learn more on DT’s website.
When you have multiple reactive variables inside a reactive context, the whole code block will get re-executed whenever any of the reactive variables change because all the variables become dependencies of the code. If you want to suppress this behavior and cause a reactive variable to not be a dependency, you can wrap the code that uses that variable inside the isolate()
function. Any reactive variables that are inside isolate()
will not result in the code re-executing when their value is changed. Read more about this behavior with ?isolate
.
Any input function has an equivalent update*Input
function that can be used to update any of its parameters.
library(shiny)
ui <- fluidPage(
sliderInput("slider", "Move me", value = 5, 1, 10),
numericInput("num", "Number", value = 5, 1, 10)
)
server <- function(input, output, session) {
observe({
updateNumericInput(session, "num", value = input$slider)
})
}
shinyApp(ui = ui, server = server)
Note that we used an additional argument session
when defining the server
function. While the input
and output
arguments are mandatory, the session
argument is optional. You need to define the session
argument when you want to use functions that need to access the session. The session
parameter actually has some useful information in it, you can learn more about it with ?shiny::session
.
Scoping is very important to understand in Shiny once you want to support more than one user at a time. Since your app can be hosted online, multiple users can use your app simultaneously. If there are any variables (such as datasets or global parameters) that should be shared by all users, then you can safely define them globally. But any variable that should be specific to each user’s session should be not be defined globally.
You can think of the server
function as a sandbox for each user. Any code outside of the server function is run once and is shared by all the instances of your Shiny app. Any code inside the server is run once for every user that visits your app. This means that any user-specific variables should be defined inside server. If you look at the code in our Virginia ABC Store app, you’ll see that we followed this rule: the raw dataset was loaded outside the server and is therefore available to all users, but the filtered
object is constructed inside the server so that every user has their own version of it. If filtered
was a global variable, then when one user changes the values in your app, all other users connected to your app would see the change happen.
You can learn more about the scoping rules in Shiny here.
If there are objects that you want to have available to both ui.R
and server.R
, you can place them in global.R
. You can learn more about global.R
and other scoping rules here.
You can add an image to your Shiny app by placing an image under the “www/” folder and using the UI function img(src = "image.png")
. Shiny will know to automatically look in the “www/” folder for the image.
If you know JavaScript or CSS you are more than welcome to use some in your app.
library(shiny)
ui <- fluidPage(
tags$head(tags$script("alert('Hello!');")),
tags$head(tags$style("body{ color: blue; }")),
"Hello"
)
server <- function(input, output) {
}
shinyApp(ui = ui, server = server)
If you do want to add some JavaScript or use common JavaScript functions in your apps, you might want to check out shinyjs.
Many people have written packages that enhance Shiny in some way or add extra functionality. Here is a list of several popular packages that people often use together with Shiny:
Shiny is a very popular package and has lots of resources on the web. Here’s a compiled list of a few resources which are all fairly easy to read and understand.
The app we developed is functional, but there are plenty of improvements that can be made. You can compare the app we developed to my version of this app to get an idea of what a (slightly) more functional app could include. Here are some suggestions of varying difficulties. Each idea also has a hint, I would recommend only reading the hint if you’re stuck for 10 minutes.
ui.R
and server.R
.
ui
variable goes into ui.R
and all the code for the server
function goes into server.R
. You do not need to explicitly call the shinyApp()
function.checkboxInput()
to get TRUE/FALSE values from the user.DT
package to turn the current results table into an interactive table.
DT
package, replace tableOutput()
with DT::dataTableOutput()
and replace renderTable()
with DT::renderDataTable()
.shinyjs::colorInput()
to let the user decide on the colors of the bars in the plot.tabsetPanel()
to create an interface with multiple tabs.www
and use the function includeCSS()
to use it in your app.shinyjs
, leaflet
, shinydashboard
, shinythemes
, ggvis
.
textOutput()
to the UI, and in its corresponding renderText()
use the number of rows in the filtered()
object.downloadButton()
and downloadHandler()
functions.typeInput
radio buttons into checkboxes (checkboxGroupInput()
) since checkboxes support choosing multiple items, or change typeInput
into a select box (selectInput()
) with the argument multiple = TRUE
to support choosing multiple options.Like many U.S. states, Virginia exercises a monopoly over all sales of hard liquor in the state (plus an assortment of some wines).↩
This work is licensed under the CC BY-NC 4.0 Creative Commons License.