That Yellow Line in VS Code Nearly Broke My Brain (And How I Finally Got It)

That Yellow Line in VS Code Nearly Broke My Brain (And How I Finally Got It)

Three hours. Three whole hours I spent staring at this yellow squiggly line in VS Code today.

You know the one I’m talking about:

React Hook useEffect has a missing dependency: 'functionName'. 
Either include it or remove the dependency array.

At first, I thought, “Easy fix! I’ll just add the function to the dependency array.”

Famous last words.

What followed was a rabbit hole of infinite loops, performance issues, and enough confusion to make me question my life choices. But by the end of it, I finally understood what React was trying to tell me.

If you’ve ever felt like throwing your laptop out the window because of this warning, this post is for you.

The “Just Add It to the Array” Trap

Let’s start with the code that started my three-hour nightmare:

useEffect(() => {
  applyFilter();
}, []); // ESLint is angry about this empty array

VS Code is screaming at me: “Hey, you’re using applyFilter but not including it in the dependency array!”

So I think, “Fine, I’ll add it”:

useEffect(() => {
  applyFilter();
}, [applyFilter]); // This should fix it, right?

WRONG. Now my app is stuck in an infinite loop, my CPU fan is going crazy, and I’m more confused than ever.

Why Does This Happen? (The Real Answer)

After banging my head against the wall for way too long, I realized I needed to go back to basics. The problem isn’t with ESLint being annoying—it’s actually trying to save us from something called stale closures.

What Are Stale Closures? (In Plain English)

Imagine you write down your friend’s phone number on a piece of paper. Later, your friend changes their number, but you still have the old one written down. When you try to call them, you’re using outdated information.

That’s exactly what happens with stale closures in React—your function “remembers” old values that are no longer accurate.

A Real Example That’ll Make You Go “Oh!”

function Counter() {
  const [count, setCount] = useState(0);
  
  const logCount = () => {
    console.log(count); // This captures whatever 'count' was when the function was created
  };
  
  useEffect(() => {
    const timer = setInterval(logCount, 1000);
    return () => clearInterval(timer);
  }, []); // Empty dependency array - this is the problem!
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

What happens:

  1. Component loads with count = 0
  2. logCount function is created, remembering count = 0
  3. User clicks button like crazy: count becomes 1, 2, 3, 10, 50…
  4. But logCount is still stuck in the past, forever logging 0

It’s like that friend who still thinks it’s 2019 and keeps making pandemic jokes.

Back to Basics: Understanding useEffect (For Real This Time)

When I got completely lost, I remembered the golden rule: go back to basics.

What Is useEffect Actually Doing?

useEffect(() => {
  console.log("Hello, I'm a side effect!");
}, []);

This runs once when the component loads. Think of it as saying, “Hey React, after you’re done setting up this component, run this code.”

The Dependency Array (That Second Parameter)

useEffect(() => {
  console.log("Count changed:", count);
}, [count]); // This array tells React: "Run this effect when 'count' changes"

The dependency array is React’s way of knowing when to re-run your effect. It’s like telling React, “Watch these variables, and if any of them change, run my code again.”

Why ESLint Gets Mad

const sayHello = () => {
  console.log("Hello!");
};

useEffect(() => {
  sayHello();
}, []); // ESLint: "Um, excuse me, you're using 'sayHello' but not watching it!"

ESLint can’t read your mind. It doesn’t know if sayHello might change between renders. To be safe, it wants you to include it in the dependency array.

But here’s where things get tricky…

The Infinite Loop Trap (Why Adding Functions Can Break Everything)

Let’s say you have this code:

const sortProducts = () => {
  const sorted = [...filteredProducts].sort((a, b) => a.price - b.price);
  setFilteredProducts(sorted);
};

useEffect(() => {
  sortProducts();
}, [sortProducts]); // This looks innocent, but it's a trap!

Here’s what happens (the infinite loop of doom):

  1. useEffect runs → calls sortProducts()
  2. sortProducts() calls setFilteredProducts()
  3. State changes → component re-renders
  4. New render creates a new version of sortProducts (different function reference)
  5. useEffect sees “new” sortProducts → runs again
  6. Back to step 1 → INFINITE LOOP 🔄

It’s like being stuck in a revolving door that never stops spinning.

The Solutions (That Actually Work)

Solution 1: useCallback (The Fancy Way)

const sortProducts = useCallback(() => {
  const sorted = [...filteredProducts].sort((a, b) => a.price - b.price);
  setFilteredProducts(sorted);
}, [filteredProducts]); // Only recreate when filteredProducts changes

useEffect(() => {
  sortProducts();
}, [sortProducts]); // Now this is safe!

How it works:

  • useCallback memoizes (remembers) the function
  • Only creates a new function when its dependencies change
  • Breaks the infinite loop because the function reference stays stable

Pros:

  • Prevents stale closures ✅
  • No infinite loops ✅
  • Function can be reused elsewhere ✅

Cons:

  • More complex syntax
  • Need to manage useCallback dependencies too

Solution 2: Put Logic Inside useEffect (The Simple Way)

useEffect(() => {
  const sorted = [...filteredProducts].sort((a, b) => a.price - b.price);
  setFilteredProducts(sorted);
}, [filteredProducts]); // Much cleaner!

How it works:

  • No external function to worry about
  • Clear what the effect depends on
  • Simple and straightforward

Pros:

  • Super simple ✅
  • No useCallback needed ✅
  • Clear dependencies ✅

Cons:

  • Can’t reuse the logic elsewhere
  • Might get messy for complex operations

The Child Component Performance Trap

Here’s something that confused me for ages: “Why do I need useCallback when passing functions to child components?”

Let me show you with an example that’ll make it click:

Without useCallback (Performance Killer)

const ProductList = () => {
  const [filter, setFilter] = useState('');
  const [products, setProducts] = useState([]);

  // This function gets recreated on EVERY render
  const handleAddToCart = (productId) => {
    console.log('Adding to cart:', productId);
  };

  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
        placeholder="Search products..."
      />
      
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart} // New function every time!
        />
      ))}
    </div>
  );
};

const ProductItem = React.memo(({ product, onAddToCart }) => {
  console.log('Rendering:', product.name); // This logs way too much!
  
  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={() => onAddToCart(product.id)}>
        Add to Cart
      </button>
    </div>
  );
});

What happens:

  1. User types in search box
  2. filter state changes → component re-renders
  3. handleAddToCart gets recreated (new function reference)
  4. ALL ProductItem components re-render because they got “new” props
  5. Performance dies a slow death 💀

With useCallback (Performance Champion)

const ProductList = () => {
  const [filter, setFilter] = useState('');
  const [products, setProducts] = useState([]);

  // This function only changes when we want it to
  const handleAddToCart = useCallback((productId) => {
    console.log('Adding to cart:', productId);
  }, []); // Empty dependencies = never recreate

  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
        placeholder="Search products..."
      />
      
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart} // Same function every time!
        />
      ))}
    </div>
  );
};

What happens now:

  1. User types in search box
  2. filter state changes → component re-renders
  3. handleAddToCart keeps the same reference
  4. ProductItem components DON’T re-render (props haven’t changed)
  5. App stays fast and responsive! 🚀

My Real-World Infinite Loop Disaster

Let me share the actual code that broke my brain today:

// This was causing the infinite loop
const sortProducts = useCallback(() => {
  let fpCopy = [...filteredProducts];

  switch (sortOption) {
    case "low-high":
      setFilteredProducts(fpCopy.sort((a, b) => a.price - b.price));
      break;
    case "high-low":
      setFilteredProducts(fpCopy.sort((a, b) => b.price - a.price));
      break;
    default:
      applyFilter(); // This also calls setFilteredProducts
      break;
  }
}, [filteredProducts, sortOption, applyFilter]); // filteredProducts is a dependency!

useEffect(() => {
  sortProducts();
}, [sortProducts]);

The problem:

  • sortProducts depends on filteredProducts
  • sortProducts changes filteredProducts
  • Changing filteredProducts recreates sortProducts
  • New sortProducts triggers useEffect again
  • INFINITE LOOP 🔄

The Fix That Saved My Sanity

const sortProducts = useCallback((productsToSort) => {
  let fpCopy = [...productsToSort];

  switch (sortOption) {
    case "low-high":
      return fpCopy.sort((a, b) => a.price - b.price);
    case "high-low":
      return fpCopy.sort((a, b) => b.price - a.price);
    default:
      return productsToSort;
  }
}, [sortOption]); // Only depends on sortOption now!

useEffect(() => {
  setFilteredProducts(prevProducts => sortProducts(prevProducts));
}, [sortProducts]);

What I changed:

  1. Made sortProducts a pure function (takes input, returns output)
  2. Removed filteredProducts from dependencies
  3. Used functional state update to work with previous state
  4. Broke the infinite loop! 🎉

The Even Simpler Solution

After all this complexity, I realized I could just combine everything into one effect:

useEffect(() => {
  // Apply filters first
  let productsCopy = products.slice();

  if (selectedCategories.length > 0) {
    productsCopy = productsCopy.filter(item =>
      selectedCategories.includes(item.category)
    );
  }

  if (selectedTypes.length > 0) {
    productsCopy = productsCopy.filter(item =>
      selectedTypes.includes(item.type)
    );
  }

  // Then apply sorting
  switch (sortOption) {
    case "low-high":
      productsCopy = productsCopy.sort((a, b) => a.price - b.price);
      break;
    case "high-low":
      productsCopy = productsCopy.sort((a, b) => b.price - a.price);
      break;
    default:
      // Keep original order
      break;
  }

  setFilteredProducts(productsCopy);
}, [products, selectedCategories, selectedTypes, sortOption]);

Sometimes the simplest solution is the best one.

Key Takeaways (So You Don’t Spend 3 Hours Like I Did)

  1. ESLint warnings exist for a reason – they prevent real bugs, not just annoy you
  2. Stale closures are real – functions can “remember” old values
  3. useCallback prevents infinite loops – when you need to pass functions around
  4. Sometimes simple is better – don’t overcomplicate with multiple effects
  5. Pure functions are your friend – they don’t cause side effects

The Golden Rules I Learned Today

Use useCallback when passing functions to child componentsUse useCallback when functions are dependencies of other hooksDon’t use useCallback for simple functions that aren’t passed aroundConsider moving logic inside useEffect for simple casesTest your components after fixing dependency warnings

Final Thoughts

That yellow line in VS Code isn’t your enemy—it’s trying to save you from bugs you didn’t even know existed. Understanding useEffect dependencies might feel like learning a new language, but once it clicks, you’ll write much more robust React applications.

And hey, at least now when you see that warning, you won’t spend three hours pulling your hair out like I did today! 😅


Have you ever been stuck in a React infinite loop? What was your “aha!” moment? I’d love to hear your stories in the comments!

Thanks for reading! — Ranjan

Leave a Comment

Your email address will not be published. Required fields are marked *