DEV Community

Ahmed Adel | Dev Mind
Ahmed Adel | Dev Mind

Posted on

The 2 AM React Loop That Almost Melted My Browser (And How Sentry Saved My CPU)

Summer Bug Smash: Smash Stories Submission 🐛🛹

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)