This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry. We’ve all been there. You build a feature, test it on your local machine with your fast internet, and it feels like butter. You feel like a 10x developer.
Then, you deploy it.
A few hours later, you open the live site on your phone, click a button, and... boom. The browser tab completely freezes. The fan on your laptop starts sounding like a jet engine preparing for takeoff.
This is the story of how a tiny, innocent-looking React hook created an infinite render loop, and how I tracked it down before my CPU melted.
The Setup: A Simple Search Filter
I was working on a dynamic dashboard project. I wanted to build a real-time filter panel where users could filter cards by category and tags.
Here is what the code looked like (simplified, but this was the logic):
const [filterOptions, setFilterOptions] = useState({ category: 'all', limit: 10 });
const [data, setData] = useState([]);
// The innocent-looking fetch hook
useEffect(() => {
api.fetchProducts(filterOptions).then(res => {
setData(res.data);
});
}, [filterOptions]); // <-- Pay attention to this guy
On paper, this looks completely fine, right? Every time the user changes the filter options, we fetch new data.
Except, it wasn't fine. It was a ticking time bomb.
The Nightmare: 100% CPU Usage
When I deployed the app and tested it under a simulated slow network (3G throttling), the site froze.
I opened Chrome DevTools. The Network tab was a crime scene. There were literally hundreds of identical API requests being fired every single second. The console was flooded, and the page was unresponsive.
Why?
Because in Javascript, objects are compared by reference, not by value.
Every time the component rendered, filterOptions was being recreated as a new object in memory, even if its properties (category and limit) hadn't changed. Because it was a "new" object reference, useEffect thought the dependency had changed, so it ran the fetch function again. The fetch function updated the state (setData), which triggered a re-render... which recreated the object, which triggered the useEffect... and the loop went infinite.
How Sentry Saved the Day (The "Aha!" Moment)
At first, I was staring at the code trying to figure out which component in my nested tree was triggering this.
Since I had Sentry integrated into the project, I decided to check the dashboard. Under the Performance tab, Sentry flagged a massive spike in "Transactions".
When I clicked on the issue, Sentry’s breadcrumbs showed me the exact flow:
componentRender -> http.client (API Call)
componentRender -> http.client (API Call)
componentRender -> http.client (API Call)
It showed this repeating 150 times within a 3-second window for the FilterPanel component. Sentry didn't just tell me "there is an error"; it showed me the exact components causing the render storm.
The Fix: Memorizing the Options
Once Sentry pointed me to the exact culprit, the fix was simple. I needed to make sure React knew the object hadn't actually changed.
I used useMemo to memoize the filter options so the object reference remained stable unless the primitive values changed:
// Fixing the reference issue
const memoizedFilters = useMemo(() => {
return { category: filterCategory, limit: 10 };
}, [filterCategory]); // Only recreates if filterCategory actually changes
useEffect(() => {
api.fetchProducts(memoizedFilters).then(res => {
setData(res.data);
});
}, [memoizedFilters]); // Now this reference is stable!
I pushed the fix. The network tab went quiet. The CPU usage dropped from 98% back to a cool 2%. My laptop fan went silent, and I could finally breathe.
What I Learned (The Hard Way)
Javascript references will bite you: Never pass non-primitive types (objects, arrays, functions) directly into dependency arrays without memoizing them (useMemo or useCallback).
Local testing is a lie: Always test your apps with network throttling enabled. Infinite loops hide behind fast networks because the browser finishes the loop faster than you can notice.
Observability is key: Without Sentry, I would have spent hours console-logging every single hook. Having visual proof of the transaction loop saved my sanity.
Have you ever created an infinite loop that almost crashed your system? Let me know in the comments!
Top comments (0)