Skip to content
R for the Humanities

To build a Shiny app for history, you write two pieces in R — a ui that lays out controls and outputs, and a server function that reacts to the user — then run them with shinyApp(). Load your historical dataset once at the top, filter it reactively, and render a table, map, or chart. You need no web-development background; Shiny writes the HTML for you. Below is a complete, minimal app for an archival dataset, then the settings and pitfalls that take it from "runs" to "usable".

What is the minimum app structure?

A Shiny app is data loaded once, a ui, and a server. Keep the data load outside the server so it is not re-read on every click.

r
library(shiny)
library(dplyr)
library(DT)

records <- readr::read_csv("records.csv")   # loaded ONCE, app-wide

ui <- fluidPage(
  titlePanel("Parish Records Explorer"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("years", "Year range",
                  min = 1700, max = 1900, value = c(1750, 1850), sep = ""),
      selectInput("parish", "Parish", choices = c("All", unique(records$parish)))
    ),
    mainPanel(DTOutput("table"), downloadButton("dl", "Download CSV"))
  )
)

How do I make the table react to the controls?

Filter inside a reactive() so the computation caches and only re-runs when an input changes. Then render the cached result.

r
server <- function(input, output, session) {
  filtered <- reactive({
    d <- records |> filter(year >= input$years[1], year <= input$years[2])
    if (input$parish != "All") d <- filter(d, parish == input$parish)
    d
  })

  output$table <- renderDT(filtered())

  output$dl <- downloadHandler(
    filename = function() paste0("extract-", Sys.Date(), ".csv"),
    content  = function(file) readr::write_csv(filtered(), file)
  )
}

shinyApp(ui, server)

filtered() is computed once per input change and reused by both the table and the download — that single pattern is what keeps a history app responsive.

Where should I host it, and what does that cost?

Match the host to the audience and budget.

OptionCostBest forCatch
shinyapps.io freeFree, capped hoursDemosSleeps; limited monthly hours
shinylive (static)Free hostingSmall datasetsRuns in-browser; data is public
Self-hosted Shiny ServerServer costInstitutional controlYou maintain it
Posit ConnectPaidTeams, authLicence cost

For a small, public historical dataset, shinylive is often the sweet spot: it compiles to WebAssembly and runs entirely in the visitor's browser, so there is no server to keep alive.

How do I keep a large dataset fast?

Three rules. Load data once outside server. Wrap every expensive step in reactive(). And pre-aggregate where you can — if you only ever show counts by decade, compute that once at startup rather than on each render. Reading a 200 MB CSV inside the server function is the classic mistake that makes an app crawl.

What pitfalls trip up historical apps specifically?

  • Dates that are not really dates — "circa 1640" and "undated" break sliders; clean to numeric years or bounds first.
  • Public data exposure — shinylive ships the whole dataset to the browser; never use it for sensitive or rights-restricted material.
  • No citation path — give every view a stable label and a download so a visitor can cite the extract.
  • Silent filtering — show the current row count so users know whether a filter returned nothing.

How do I add a map or timeline?

Drop in leaflet for places (leafletOutput/renderLeaflet) or plotly/ggplot2 for time series, driven by the same filtered() reactive. Reuse the reactive rather than re-filtering per output, and the map, table and chart stay in sync automatically.

Key Takeaways

  • A Shiny app is a ui plus a server, launched with shinyApp().
  • Load historical data once outside the server so it is not re-read on each click.
  • Wrap filtering and heavy work in reactive() so results cache and stay fast.
  • downloadHandler() plus downloadButton() lets visitors take citable extracts.
  • shinylive runs in the browser with no server — great for small public datasets.
  • Clean uncertain dates to numeric years before wiring them to sliders.
  • Show the live row count so users notice when a filter returns nothing.

Frequently Asked Questions

Do I need to know web development to build a Shiny app?

No. Shiny generates the HTML, CSS and JavaScript for you from R code. You write a ui object and a server function; basic R is enough to ship a working interactive app, though some HTML helps with polish.

Where can I host a Shiny app for free?

shinyapps.io offers a free tier with limited active hours per month, which suits a project demo. For a permanent public exhibit, Posit Connect, a self-hosted Shiny Server, or shinylive (which runs entirely in the browser with no server) are better.

Why is my Shiny app slow when the dataset is large?

You are probably re-reading or re-filtering the full data on every input change. Load data once outside the server function and wrap expensive computations in reactive() so they cache and only re-run when their inputs change.

What is the difference between reactive() and observe()?

reactive() returns a value that other expressions read and caches it; observe() runs for its side effects (like updating a plot or writing a log) and returns nothing. Use reactive() for derived data, observe()/observeEvent() for actions.

How do I let users download filtered historical data?

Use downloadHandler() in the server paired with downloadButton() in the UI. Generate the filtered data frame inside the handler's content function and write it with write.csv so visitors can take a citable extract away.

Can a Shiny app run without a server using shinylive?

Yes. shinylive compiles your app to WebAssembly so it runs fully in the visitor's browser, deployable as static files on GitHub Pages or Netlify. It is ideal for small historical datasets and avoids ongoing hosting costs.