Skip to Content
Course content

138: Real-Time Data Streaming in Shiny

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first move from static data analysis to building "live" dashboards, there is one mistake I see almost every developer make. They treat the Shiny server like a standard script. They think, "I want this plot to update every second, so I'll just wrap the data fetching logic in a while(TRUE) loop inside the server function."

The "Infinite Loop" Trap

It seems logical on paper, but in practice, it's a disaster. If you write a while loop inside your Shiny server, you aren't "streaming" data; you are hijacking the entire R process. Because R is single-threaded, that loop will occupy the CPU completely. Shiny's event loop—the mechanism that actually listens for user clicks and sends updates to the browser—never gets a chance to run. The result? Your UI freezes, the browser eventually times out, and your app becomes a very expensive space heater.

# DON'T DO THIS
server <- function(input, output, session) {
  output$plot <- renderPlot({
    while(TRUE) { 
      data <- fetch_sensor_data() 
      plot(data) 
      Sys.sleep(1) # You think this helps, but it still blocks the server!
    }
  })
}

Letting Shiny Breathe with invalidateLater

The trick to real-time streaming in Shiny is to stop trying to push data and start letting Shiny pull data on a schedule. We do this using invalidateLater(). Instead of a loop that never ends, you create a reactive expression that tells Shiny: "I'm done for now, but please mark me as invalid (expired) in exactly 1,000 milliseconds."

When the timer expires, Shiny realizes the data is stale and re-executes only that specific reactive block. This allows the server to handle other user inputs in between updates, keeping the app responsive. I usually prefer this over reactiveTimer because it's more flexible when you need to dynamically change the update frequency based on a user's input.

Building a Non-Blocking Data Stream

Let's look at a concrete example. Suppose we're monitoring a simulated temperature sensor that fluctuates over time. We want a live plot that scrolls as new data arrives without locking up the browser.

library(shiny)
library(ggplot2)

ui <- fluidPage(
  titlePanel("Live Sensor Feed"),
  plotOutput("sensorPlot")
)

server <- function(input, output, session) {
  # We use a reactiveVal to store the accumulating stream
  sensor_data <- reactiveVal(data.frame(time = Sys.time(), temp = 20))
  
  # This reactive expression handles the "streaming" logic
  observe({
    # Tell Shiny to re-run this block every 500ms
    invalidateLater(500, session)
    
    # Simulate fetching a new reading from an API or Database
    new_reading <- data.frame(
      time = Sys.time(), 
      temp = 20 + rnorm(1, mean = 0, sd = 2)
    )
    
    # Append the new reading to our existing data
    updated_df <- rbind(sensor_data(), new_reading)
    
    # Keep only the last 50 points so the plot doesn't get cluttered
    if(nrow(updated_df) > 50) {
      updated_df <- tail(updated_df, 50)
    }
    
    sensor_data(updated_df)
  })
  
  output$sensorPlot <- renderPlot({
    ggplot(sensor_data(), aes(x = time, y = temp)) +
      geom_line(color = "steelblue", size = 1) +
      theme_minimal() +
      labs(title = "Real-time Temperature Stream", x = "Time", y = "Celsius")
  })
}

shinyApp(ui, server)

Notice how the observe block acts as a heartbeat. It updates the reactiveVal, which in turn triggers the renderPlot. The key here is that between that 500ms window, R is completely free to handle other tasks. If you had a slider to change the sensor's threshold, it would feel snappy and responsive because the server isn't trapped in a while loop.




📋 Practical Task

Build a Real-Time Cryptocurrency Price Simulator

Your task is to build a Shiny application that simulates a live price feed for a cryptocurrency (e.g., "ShinyCoin").

  • The Data: Create a reactiveVal that starts with a single price point of 100.
  • The Stream: Use invalidateLater to update the price every 200 milliseconds. The new price should be the previous price plus a small random change (e.g., rnorm(1, 0, 0.5)).
  • The UI:
    • Display the current price in a large textOutput.
    • Display a plotOutput showing the price trend over the last 100 iterations.
  • The Twist: Add a numericInput that allows the user to change the update frequency (the milliseconds in invalidateLater) in real-time without restarting the app.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.