10  Shiny Apps

10.1 Introduction

Shiny is an R package for building interactive web applications without requiring any web development experience.
It is ideal for dashboards, teaching tools, and interactive reporting.

Learning Resources:

Tip

Quick Start

To try Shiny, open R and run:

shiny::runExample("01_hello")

This launches a simple interactive histogram app.


10.2 Structure of a Shiny App

A Shiny app links UI (User Interface) and Server (Computation)
through reactive communication.

# app.R: single-file app
library(shiny)

ui <- fluidPage(
  titlePanel("Hello Shiny"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("obs", "Observations:", 1, 100, 50)
    ),
    mainPanel(
      plotOutput("distPlot")
    )
  )
)

server <- function(input, output) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
}

shinyApp(ui, server)
Note

How it works

  • The UI defines layout and input/output elements.
  • The Server defines computations and reactivity.
  • The shinyApp(ui, server) call connects them.
Tip

Two-file alternative

You may also structure an app with two files in the same folder:

  • ui.R: defines ui
  • server.R: defines server

This helps with larger apps and collaboration.


10.3 Inputs and Outputs

Inputs are widgets through which users interact.
Outputs display dynamically updated results.

Function Type Example
textInput() Input Text boxes
sliderInput() Input Sliders for numeric values
selectInput() Input Drop-down menus
renderPlot() Output Displays plots
renderTable() Output Displays data tables
renderText() Output Displays text summaries
input$slider  # numeric
input$check   # logical
Tip

Widget Gallery

See the full input/output list at
https://gallery.shinyapps.io/081-widgets-gallery/.


10.4 Reactivity

o Reactivity is the core concept of Shiny:
outputs automatically update when inputs change.

10.4.1 Reactive Expressions

rv <- reactive({ rnorm(input$obs) })
output$plot <- renderPlot(hist(rv()))

10.4.2 isolate()

Breaks the reactive chain — useful when you want
a calculation to occur only on a trigger (e.g., a button).

output$summary <- renderText({
  paste0(
    "input$text is '", input$text,
    "', and input$n is ", isolate(input$n)
  )
})

10.4.3 observeEvent()

Observers perform side-effect actions (e.g., pop-ups, saving files).

observeEvent(input$go, {
  showModal(modalDialog("You clicked Go!"))
})
Note

Tip:
Use reactive() for values, observe() for actions.

10.5 Examples

10.5.1 Example 1: Normal Distribution Explorer

This is the simplest Shiny app. It lets users change the mean and standard deviation of a normal distribution and immediately see the updated histogram.

The UI side:

Code
library(shiny)

ui <- fluidPage(
  titlePanel("Normal Distribution Explorer"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("mean", "Mean (μ):", min = -5, max = 5, value = 0, step = 0.1),
      sliderInput("sd", "Standard deviation (σ):", min = 0.1, max = 5, value = 1, step = 0.1)
    ),
    mainPanel(
      plotOutput("hist")
    )
  )
)

The Server side:

Code
server <- function(input, output) {
  output$hist <- renderPlot({
    x <- rnorm(1000, mean = input$mean, sd = input$sd)
    hist(x, main = "Histogram of Random Normal Data",
         xlab = "Value", col = "skyblue", border = "white")
  })
}

Run the App:

Code
shinyApp(ui, server)

10.5.2 Example 2: Comparing Two Distributions

This second app is adapted from the original version. It lets users control the parameters of two normal distributions and visualize their frequency polygons side by side.

The UI side:

Code
library(shiny)
library(ggplot2)
library(tibble)

ui <- fluidPage(
  titlePanel("Compare Two Normal Distributions"),
  sidebarLayout(
    sidebarPanel(
      numericInput("n1", "Sample size (Dist 1)", value = 1000, min = 100),
      numericInput("mean1", "μ (Dist 1)", value = 0, step = 0.1),
      numericInput("sd1", "σ (Dist 1)", value = 1, min = 0.1, step = 0.1),

      numericInput("n2", "Sample size (Dist 2)", value = 1000, min = 100),
      numericInput("mean2", "μ (Dist 2)", value = 1, step = 0.1),
      numericInput("sd2", "σ (Dist 2)", value = 0.5, min = 0.1, step = 0.1),

      sliderInput("range", "X-axis range", value = c(-5, 5), min = -10, max = 10),
      actionButton("go", "Generate Samples")
    ),
    mainPanel(
      plotOutput("plot2"),
      verbatimTextOutput("ttest")
    )
  )
)

The Server side:

Code
server <- function(input, output) {

  # Reactive data: generate both samples together only when button clicked
  samples <- reactive({
    input$go
    isolate({
      tibble(
        x = c(rnorm(input$n1, input$mean1, input$sd1),
              rnorm(input$n2, input$mean2, input$sd2)),
        grp = rep(c("Dist 1", "Dist 2"), c(input$n1, input$n2))
      )
    })
  })

  # Plot: smooth density curves
  output$plot2 <- renderPlot({
    req(samples())
    ggplot(samples(), aes(x = x, color = grp, fill = grp)) +
      geom_density(alpha = 0.25, linewidth = 1) +
      scale_color_manual(values = c("Dist 1" = "blue", "Dist 2" = "red")) +
      scale_fill_manual(values = c("Dist 1" = "blue", "Dist 2" = "red")) +
      coord_cartesian(xlim = input$range) +
      labs(title = "Two Normal Distributions",
           x = "Value", y = "Density", color = "Distribution", fill = "Distribution") +
      theme_minimal(base_size = 14)
  }, res = 96)

  # t-test: reuse the same simulated data
  output$ttest <- renderPrint({
    req(samples())
    t.test(x ~ grp, data = samples())
  })
}

Run the App:

Code
shinyApp(ui, server)

10.5.3 Example 3: Reactivity

This example illustrate how reactivity:

  • reactive() defines a reactive expression — a cached computation that automatically re-runs when its inputs change and can be reused across outputs.
  • isolate() lets you access reactive inputs without triggering a re-execution. Useful when you want a computation to happen only when explicitly requested.
  • observeEvent() is used to perform an action (not return a value) when an input changes — like printing to console, showing a message, or updating another output.
Code
library(shiny)

ui <- fluidPage(
  titlePanel("Reactive, Isolate, and ObserveEvent Together"),
  numericInput("mean", "Mean:", 0),
  numericInput("sd", "SD:", 1, min = 0.1),
  actionButton("go", "Generate Sample"),
  plotOutput("hist"),
  verbatimTextOutput("summary")
)

server <- function(input, output) {
  # reactive container to hold the current dataset
  data <- reactiveVal(NULL)

  # observeEvent: generate new data only when button clicked
  observeEvent(input$go, {
    m <- isolate(input$mean)
    s <- isolate(input$sd)
    data(rnorm(1000, m, s))
  })

  # outputs that react to data()
  output$hist <- renderPlot({
    req(data())  # only plot if available
    hist(data(), main = "Generated Data", col = "lightgreen")
  })

  output$summary <- renderPrint({
    req(data())
    summary(data())
  })
}

shinyApp(ui, server)

10.6 Deployment

Deploying a Shiny app means making it available to others.

10.6.1 Traditional Deployment

  • shinyapps.io
  • Posit Connect (enterprise hosting)
  • Private server (e.g., university or company)

10.6.2 Shinylive Deployment

Shinylive runs entirely in the browser (no R server needed).

install.packages("shinylive")
shinylive::export("app", "site")

Then deploy the site folder like a static website.

Tip

Limitations of Shinylive

  • Not all R packages are supported.
  • Initial loading may be slow for large apps.

10.7 Best Practices

  • Write automated tests with
    shinytest2
  • Build apps as packages for easier dependency management.
  • Use modules to reuse UI and server components.
  • Learn a little HTML/CSS/JS for customization.
  • Start with a sketch—plan your app’s user experience before coding.

10.8 Hands-on Exercise

Tip

Exercise: Build Your First App

  1. Create a new file called app.R
  2. Define ui and server as shown above
  3. Add shinyApp(ui, server) at the end
  4. Run the app and change the slider to see live updates