Forgetting dependency array in useEffect, causing infinite calls

Loading

Managing Dependency Arrays in useEffect to Prevent Infinite Loops

A common mistake with React’s useEffect hook is forgetting or incorrectly specifying the dependency array, which can lead to infinite re-renders or stale closures.

The Core Problem

// ❌ Missing dependency array - runs after every render
useEffect(() => {
  fetchData();
}); 

// ❌ Incorrect dependencies - infinite loop
useEffect(() => {
  setCount(count + 1);
}, [count]); // Updates count, which triggers effect again

Proper Usage Patterns

1. Empty Dependency Array (Mount Only)

// ✅ Runs only once when component mounts
useEffect(() => {
  fetchInitialData();
}, []);

2. Complete Dependencies

// ✅ Runs when userId or page changes
useEffect(() => {
  fetchUserData(userId, page);
}, [userId, page]); // All dependencies declared

3. Functional Updates to Avoid Dependencies

// ✅ No count in dependencies
useEffect(() => {
  const interval = setInterval(() => {
    setCount(prev => prev + 1); // Functional update
  }, 1000);
  return () => clearInterval(interval);
}, []); // Empty array = runs once on mount

Common Scenarios and Fixes

1. Fetching Data

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // ✅ Correct dependencies
  useEffect(() => {
    const loadUser = async () => {
      const data = await fetchUser(userId);
      setUser(data);
    };
    loadUser();
  }, [userId]); // Re-runs only when userId changes
}

2. Event Listeners

function ScrollWatcher() {
  // ✅ Cleanup function with stable handler
  useEffect(() => {
    const handleScroll = throttle(() => {
      console.log(window.scrollY);
    }, 100);

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []); // Empty array = setup/cleanup once
}

3. Working with Objects/Dependencies

function Chat({ currentRoom }) {
  const [messages, setMessages] = useState([]);
  const room = { id: currentRoom, name: `Room ${currentRoom}` };

  // ❌ Problem - new object every render
  useEffect(() => {
    subscribeToRoom(room);
    return () => unsubscribeFromRoom(room);
  }, [room]); // Infinite loop - room object changes

  // ✅ Solution 1 - use primitive values
  useEffect(() => {
    subscribeToRoom(currentRoom);
    return () => unsubscribeFromRoom(currentRoom);
  }, [currentRoom]);

  // ✅ Solution 2 - memoize object
  const stableRoom = useMemo(() => ({
    id: currentRoom,
    name: `Room ${currentRoom}`
  }), [currentRoom]);
}

Best Practices

  1. Always include all dependencies that your effect uses
  2. Use the exhaustive-deps ESLint rule to catch missing dependencies
  3. Split complex effects into multiple smaller effects
  4. Memoize dependencies with useMemo when passing objects/arrays
  5. Use functional updates for state setters to avoid dependencies
  6. Move functions inside effects if they don’t need to be reused

Debugging Infinite Loops

If your effect runs in an infinite loop:

  1. Check if any dependency changes on every render
  2. Verify you’re not updating a dependency within the effect
  3. Consider if you need the effect at all (could the computation happen during render?)
  4. Use console.log to track dependency changes:
useEffect(() => {
  console.log('Dependencies changed:', userId, page);
  fetchData(userId, page);
}, [userId, page]);

Remember that the dependency array is how React knows when to re-run your effect. Getting it right prevents bugs while maintaining optimal performance.

Leave a Reply

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