LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #3: Infinite Loops – When useEffect turns the browser into a 'furnace'
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

Blog #3: Infinite Loops – When useEffect turns the browser into a 'furnace'

The story of a 'harmless' line of code that made the laptop's fan sound like a tractor and the console log fly faster than the speed of light.

TP
Truong PhamSoftware Engineer
PublishedMarch 15, 2024

There's a moment every React developer has experienced: You just saved a file, the browser auto-refreshes, and suddenly... your laptop's cooling fan starts roaring like a jet engine. Your mouse starts lagging, the screen freezes, and when you try to open the Console Log, you see millions of notifications flowing past so fast you can't even read them.

Congratulations, you've just created an Infinite Loop with useEffect.

I once "proudly" crashed the whole QA team's browsers during a demo just because of a misplaced square bracket. It was a costly lesson in "thinking I understood React."

1. The Essence of Chaos: Referential Equality

To understand why useEffect runs forever, we have to talk about how React compares data. When you pass a dependency array to useEffect, React compares each element with its old value using the === operator.

The problem is: [] === [] is always false. {} === {} is also always false.

In JavaScript, reference types (Object, Array, Function) always create a new memory address every time the component re-renders. And that is the spark for infinite loops.

2. The Problem: Data fetching and the "Object Dependency" trap

Our system at the time had a filter function for a course list. I wanted to call the API to get new data whenever the user changed the filters. The code looked very "clean" like this:

// The "modern" code that cost me dearly
const [data, setData] = useState([]);
const filters = { category: 'frontend', level: 'beginner' }; // Terrible: Initializing object inside the component

useEffect(() => {
  fetchData(filters).then(res => setData(res));
}, [filters]); // React: "Oh, new filters? Run it!" -> setData -> Re-render -> new filters -> Run again...

Everything looked very logical on the surface. But because filters is an object that is recreated on every render, React mistakenly thinks the filters have changed and triggers useEffect. After fetching, I called setData, causing the component to re-render, creating a new filters... and just like that, my app fell into a bottomless black hole.

3. The Process: Debugging in the "Eye of the Storm"

When the browser freezes, the first thing I do isn't reading the code but... closing that tab as fast as possible to save the CPU. Then, I added a "holy" log line to confirm my suspicion:

console.log('useEffect triggered at:', Date.now())

If the number of logs increases by thousands in just 1 second, I knew I was in trouble.

I tried using JSON.stringify(filters) as a dependency—a "hacky" way I found on Stack Overflow. It actually worked, but I felt something was very wrong. It felt like using duct tape to fix a burst water pipe.

The Senior-level fix: I realized I needed to stabilize the reference of filters. Either move it out of the component (if it's a constant) or use useMemo. However, the best way is often breaking the dependency.

// Sustainable solution
useEffect(() => {
  // Instead of depending on the whole object, only depend on primitive values
  fetchData(filters.category, filters.level).then(res => setData(res));
}, [filters.category, filters.level]); 

4. Counter-argument: Sometimes we are too obsessed with useEffect

Looking back, I realized I used to be "addicted" to useEffect. I used it for everything: from calculating data, synchronizing state, to validating forms.

In fact, there are many things that don't need useEffect. If you can calculate a value from props or existing state right during render, do that. Don't force React to run an extra loop of logic after it has already drawn the UI. It's a waste and also where many bugs arise.

5. Junior vs Senior: Difference in responsibility

A Junior developer often uses // eslint-disable-next-line react-hooks/exhaustive-deps to disable plugin warnings. That's extremely dangerous. It's like removing the battery from a fire alarm because it's too loud when you're cooking.

A Senior developer sees ESLint warnings as "advice from a wise friend." If it reports a missing dependency, they don't ignore it—they find a way to include it safely (using useCallback, useMemo, or restructuring the logic).

6. Lessons Learned

Infinite loops aren't as scary as not understanding the mechanism of the tool you're using.

  • Rule #1: Always prioritize primitive references (string, number, boolean) in the dependency array.
  • Rule #2: If you must use Objects/Arrays, make sure they are thoroughly memoized.
  • Rule #3: If you find yourself writing too many useEffect instances, your state structure might be problematic.

Nowadays, whenever a laptop's cooling fan gets loud, I don't panic anymore. I just smile, knowing I've found another way... not to crash an app.


Getting code to run is good, but getting it to run the right number of times is the absolute peak.

Series • Part 3 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #4: Hydration Error – When SSR's perfection 'slaps' me in the face at midnight
Blog #2: The 60 FPS Illusion – When your MacBook Pro 'deceives' your senses
01Blog #1: When 'It works on my machine' is the sweetest lie02Blog #2: The 60 FPS Illusion – When your MacBook Pro 'deceives' your senses03Blog #3: Infinite Loops – When useEffect turns the browser into a 'furnace'Reading04Blog #4: Hydration Error – When SSR's perfection 'slaps' me in the face at midnight05Blog #5: 200 OK – The system's sweet little lies06Blog #6: Why you shouldn't fully trust console.log – The silent deceiver07Blog #7: The 'Double' Nightmare – When React StrictMode tests your patience08Blog #8: Memory Trash – The price of forgetting to 'clean up' your Event Listeners
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles