Interactive Apps with Shiny

Reviewed & published by Brayan K

Shiny is an R package for building interactive web applications, pairing a UI that defines inputs and outputs with a reactive server that recomputes results automatically whenever a user changes an input.

Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll understand the ui/server split, the reactive model, how inputs and outputs connect, and how to build and launch a small interactive app with shinyApp().

What You'll Learn in This Lesson

1️⃣ The Three Parts of an App

Every Shiny app is a ui (the layout the user sees), a server function (the R logic), and a call to shinyApp() that launches them together. Inputs in the UI carry an inputId; the server reads them via input$id and writes results to output$id.

2️⃣ Inputs, Outputs, and Reactivity

Inputs such as sliderInput() send values to the server; render*() functions like renderPlot() compute results, which a matching *Output() placeholder displays. Because Shiny is reactive, any output that mentions input$bins automatically re-runs when the slider moves — no manual refresh.

# Shiny needs a browser and a running server, so the app above cannot
# start here. Its central idea can: reactivity is a dependency graph that
# re-runs the outputs which read an input that changed — and nothing else.
# This is that graph, in base R, so you can watch it decide.

make_app <- function() {
  input   <- new.env()
  runs    <- new.env()
  deps    <- list()
  outputs <- list()

  set_input <- function(name, value) {
    assign(name, value, envir = input)
    for (out in names(outputs)) {
      if (name %in% deps[[out]]) {
        assign(out, get0(out, envir = runs, ifnotfound = 0) + 1, envir = runs)
        cat(sprintf("  re-rendered %-9s -> %s\n", out, outputs[[out]](input)))
      }
    }
  }
  list(
    define = function(name, reads, render) {
      deps[[name]]    <<- reads
      outputs[[name]] <<- render
      assign(name, 0, envir = runs)
    },
    set   = set_input,
    count = function(name) get(name, envir = runs)
  )
}

app <- make_app()
app$define("greeting", reads = "name",
           render = function(i) paste0("Hello, ", get("name", envir = i), "!"))
app$define("shout",    reads = "name",
           render = function(i) toupper(get("name", envir = i)))
app$define("total",    reads = "n",
           render = function(i) get("n", envir = i) * 2)

cat("set name = Ada\n");   app$set("name", "Ada")
cat("set n = 21\n");       app$set("n", 21)
cat("set name = Grace\n"); app$set("name", "Grace")

cat("\nrender counts\n")
cat(sprintf("  greeting: %d\n", app$count("greeting")))
cat(sprintf("  shout:    %d\n", app$count("shout")))
cat(sprintf("  total:    %d\n", app$count("total")))

# That last block is the whole point. "name" changed twice, so the two
# outputs reading it rendered twice each. "total" reads "n", which changed
# once, so it rendered once — Shiny never recomputed it just because
# something else on the page moved. renderText() and friends are doing
# exactly this bookkeeping for you.
library(shiny)

# Inputs feed the server; outputs are RENDERED back to the UI.
# Shiny is REACTIVE: when an input changes, every output that uses it
# automatically re-computes. You never write an update loop yourself.

ui <- fluidPage(
  titlePanel("Histogram Explorer"),
  sliderInput("bins", "Number of bins:", min = 5, max = 50, value = 20),
  plotOutput("hist")          # placeholder, filled by output$hist
)

server <- function(input, output) {
  output$hist <- renderPlot({
    hist(rnorm(500), breaks = input$bins,   # reacts to the slider
         col = "steelblue", main = "500 random values")
  })
}

shinyApp(ui, server)

# Drag the slider and the histogram redraws instantly — that's reactivity.

3️⃣ Sharing Work with reactive()

When several outputs need the same intermediate value, wrap it in reactive(). The expression runs once per change and caches its result, so a plot and a summary can share one data set instead of computing it twice. You access a reactive by calling it like a function: sample_data().

library(shiny)

# reactive() builds a value that other outputs share. It runs ONCE per change
# and caches the result, so you don't repeat expensive work in every output.

ui <- fluidPage(
  sliderInput("n", "Sample size:", min = 10, max = 1000, value = 100),
  textOutput("avg"),
  plotOutput("plot")
)

server <- function(input, output) {
  # A reactive expression: computed once, reused by both outputs
  sample_data <- reactive({
    rnorm(input$n)
  })

  output$avg  <- renderText({ paste("Mean:", round(mean(sample_data()), 2)) })
  output$plot <- renderPlot({ hist(sample_data(), col = "tomato") })
}

shinyApp(ui, server)

# Note: call a reactive like a function -> sample_data()

Your turn. Fill in the ___ lines, then run the app in RStudio.

# 🎯 YOUR TURN — complete the app, then run it in RStudio.
library(shiny)

ui <- fluidPage(
  titlePanel("Tip Calculator"),
  numericInput("bill", "Bill amount:", value = 50),
  sliderInput("pct", "Tip %:", min = 0, max = 30, value = 15),
  textOutput("tip")
)

server <- function(input, output) {
  output$tip <- renderText({
    # compute the tip = bill * (pct / 100)
    tip <- input$bill * (input$pct / 100)
    paste0("Tip: $", round(tip, 2))
  })
}

# launch the app
shinyApp(ui, server)

# 👉 In RStudio this opens a window. Change the bill or drag the slider
#    and the tip updates instantly — no refresh, no loop.

Mini-Challenge: BMI App

Build a complete small app from the outline: two numeric inputs, a shared reactive(), and a text output. It ties together everything in this lesson.

# 🎯 MINI-CHALLENGE: build a "BMI" mini-app.
#
# library(shiny)
#
# UI should have:
#   - numericInput("weight", "Weight (kg):", value = 70)
#   - numericInput("height", "Height (m):",  value = 1.75)
#   - textOutput("bmi")
#
# Server should:
#   - Use a reactive() that computes weight / height^2
#   - renderText() a message like "Your BMI is 22.9"
#
# Launch with shinyApp(ui, server).
#
# Stretch: add a verbatimTextOutput that also prints a category
#          (Underweight / Normal / Overweight) using if/else.

# your full app here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Shiny

TaskR Syntax
App pagefluidPage(...)
Slider inputsliderInput("bins", ...)
Plot output slotplotOutput("hist")
Render a plotoutput$hist <- renderPlot({ ... })
Shared reactived <- reactive({ ... }); d()
Launch appshinyApp(ui, server)

🎉 Lesson Complete!

Practice quiz

What are the three parts of every Shiny app?

  • model, view, controller
  • head, body, footer
  • ui, server, and a shinyApp() call
  • input, output, plot

Answer: ui, server, and a shinyApp() call. A Shiny app is a ui object, a server function, and a shinyApp() launcher.

What is the job of the ui object?

  • Run calculations
  • Describe inputs and output placeholders the user sees
  • Connect to the database
  • Start the web server

Answer: Describe inputs and output placeholders the user sees. The ui defines layout, inputs, and output slots; it does no computation.

How does the server read a value from an input with id 'name'?

  • get("name")
  • ui$name
  • input$name
  • server$name

Answer: input$name. Inputs are read via input$id inside the server.

Where does the server write results so the UI can show them?

  • input$id
  • render$id
  • ui$id
  • output$id

Answer: output$id. The server assigns rendered results to output$id.

What does 'reactive' mean in Shiny?

  • Outputs auto-recompute when their inputs change
  • Code runs top to bottom once
  • The UI polls the server every second
  • Errors are retried automatically

Answer: Outputs auto-recompute when their inputs change. Shiny re-runs only the outputs that depend on a changed input, with no manual loop.

Which function pairs with plotOutput() to draw the plot?

  • drawPlot()
  • renderPlot()
  • plotRender()
  • makePlot()

Answer: renderPlot(). renderPlot() computes the plot that plotOutput() displays.

When should you use reactive()?

  • To build the UI layout
  • To launch the app
  • To share or cache an intermediate value across outputs
  • To install packages

Answer: To share or cache an intermediate value across outputs. reactive() runs once per change and caches, so multiple outputs reuse the result.

How do you access the current value of a reactive named sample_data?

  • sample_data()
  • sample_data$value
  • get(sample_data)
  • $sample_data

Answer: sample_data(). You call a reactive like a function: sample_data().

Which call launches a Shiny app?

  • runApp(ui)
  • launch(server)
  • shinyApp(ui, server)
  • start(ui, server)

Answer: shinyApp(ui, server). shinyApp(ui, server) wires the two parts together and starts the app.

Why can't a Shiny app run on a one-shot snippet website?

  • It needs a paid license
  • Shiny is a live web server needing a running R process and browser connection
  • Snippet sites block the shiny package
  • It only runs on Windows

Answer: Shiny is a live web server needing a running R process and browser connection. Shiny apps host a live server with a browser connection, which snippet sites cannot provide.

Continue this course

Frequently asked questions

What is the difference between the ui and the server in a Shiny app?

The ui object describes what the user sees — inputs like sliders and text boxes, plus placeholders where outputs will appear. It produces HTML. The server is an R function that contains the logic: it reads values from input$, computes results, and writes them to output$. The golden rule is that the UI never does calculations and the server never builds layout. shinyApp() connects the two and starts the web server.

What does 'reactive' actually mean in Shiny?

Reactive means outputs automatically stay in sync with inputs without you writing any update code. When a user changes an input (say, drags a slider), Shiny notices and re-runs only the outputs that depend on that input. You declare the relationship once inside renderPlot() or renderText(), and Shiny handles the 'when' for you. This is fundamentally different from a normal script that runs top to bottom once — a Shiny app is a living graph of dependencies.

When should I use reactive() instead of just computing inside renderPlot?

Use reactive() when two or more outputs need the same intermediate result, or when a computation is expensive. A reactive expression runs once per change and caches its value, so both a plot and a summary table can share the same filtered data without recomputing it twice. You call a reactive like a function — sample_data() — wherever you need its current value. Without it, you'd duplicate the same code in every render block.

Why can't I run a Shiny app on a code snippet website?

Shiny apps are interactive web servers, not one-shot scripts: they open a live connection between a browser and a running R process, listening for input changes and pushing updates back. Snippet sites that just evaluate code and print a result have nowhere to host that running server or render the interactive page. To see a Shiny app you run it locally in RStudio (which opens a browser window), deploy it to shinyapps.io or a Shiny Server, or use shinylive to compile it to run entirely in the browser.