R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
43: Reactive Programming in Shiny
If you've spent any time in standard R, you're used to linear execution: line one runs, then line two, then line three. Shiny throws that out the window. It uses a reactive graph, which is essentially a map of dependencies. When an input changes, Shiny looks at that map to figure out exactly which outputs need to be updated. If you treat a Shiny app like a linear script, you're going to run into performance bottlenecks that will make your app feel sluggish, even with small datasets.
The temptation to just put it in the render function
When I first started with Shiny, my instinct was to keep things simple. If I wanted a plot based on a filtered dataset, I'd put the filtering logic right inside the renderPlot block. It feels intuitive because everything related to that plot is in one place. Let's look at a scenario where we're analyzing a dataset of city temperatures. You have a slider for the year and a dropdown for the city.
# The naive approach
output$tempPlot <- renderPlot({
# We filter the data inside the render function
filtered_data <- my_big_dataset %>%
filter(year == input$selected_year,
city == input$selected_city)
# Then we plot it
ggplot(filtered_data, aes(x = month, y = temp)) + geom_line()
})
At first glance, this works perfectly. The plot updates when the year or city changes. But here is where the problem starts: what happens if you have a second output, like a table showing the summary statistics of that same filtered data? You'd likely copy-paste that filtering logic into a renderTable block. Now, every time a user changes an input, Shiny is filtering the same massive dataset twice. It's redundant, it's messy, and it's a waste of CPU cycles.
Why the "everything-in-one-place" approach kills performance
The real danger isn't just redundancy; it's the lack of caching. In the example above, the filtering logic is tied directly to the output. If you add a third input—say, a checkbox to toggle the plot's gridlines—the renderPlot function will trigger again. Shiny doesn't know that the gridline toggle doesn't affect the data filtering, so it re-runs the entire block, re-filtering the data from scratch just to change a line on the graph. I've seen apps crawl to a halt because they were re-calculating complex regressions or querying databases inside render functions every time a user clicked a trivial UI element.
Decoupling calculations with reactive expressions
The professional way to handle this is to move the "heavy lifting" into a reactive() expression. Think of a reactive expression as a smart variable. It doesn't just store a value; it remembers what inputs it depends on and it caches the result. If the inputs it depends on haven't changed, Shiny just hands back the cached value without re-running the code.
# The professional approach
# 1. Create a reactive expression for the filtered data
filtered_data <- reactive({
# This only re-runs if input$selected_year or input$selected_city changes
my_big_dataset %>%
filter(year == input$selected_year,
city == input$selected_city)
})
# 2. Use that reactive expression in your outputs
output$tempPlot <- renderPlot({
# Note the parentheses: we call filtered_data() as a function
ggplot(filtered_data(), aes(x = month, y = temp)) + geom_line()
})
output$tempTable <- renderTable({
filtered_data() %>% summarize(avg_temp = mean(temp))
})
By doing this, we've created a single point of truth. If the user changes the city, the filtered_data reactive updates, which then triggers both the plot and the table to update. But if the user changes something unrelated—like that gridline checkbox—the renderPlot function still runs, but when it calls filtered_data(), Shiny says, "I already have that result cached," and skips the filtering step entirely. It's a massive win for efficiency.
When to use observeEvent instead of reactive
One final point that often trips people up: the difference between reactive() and observeEvent(). Use reactive() when you want to calculate a value to be used elsewhere. Use observeEvent() when you want to perform an action—a "side effect"—like showing a pop-up notification, saving a file to a disk, or updating another input value using updateSelectInput. If you find yourself trying to assign a value to a variable inside an observeEvent and then using that variable in a plot, you're fighting the framework. Switch back to a reactive expression.
📋 Practical Task
Refactoring the Global Weather Dashboard
You have been handed a legacy Shiny app that is performing poorly. Currently, the app filters a large weather dataset and calculates the monthly average temperature inside three separate render functions: renderPlot, renderText (for the max temperature), and renderTable (for the monthly breakdown). This means the filtering logic is running three times on every input change.
Your Task:
- Identify the repeated filtering and calculation logic across the three output functions.
- Create a single
reactive()expression namedprocessed_weatherthat handles the filtering and the average temperature calculation. - Update the
renderPlot,renderText, andrenderTablefunctions to callprocessed_weather()instead of recalculating the data. - Verify that the app still produces the same results but now utilizes Shiny's caching mechanism.
There are no comments for now.