🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

React useEffect Infinite Loop — Causes and Fixes (2026)

⏱️3 min read  ·  549 words

React useEffect Infinite Loop — Causes and Fixes (2026)

Symptom: Component re-renders endlessly. Browser slows to a crawl. Console shows repeated API calls or state updates.

Root Cause

Infinite loops in useEffect happen when: the effect runs → causes a state/prop change → component re-renders → effect runs again → repeat forever.

Cause 1: Missing or Wrong Dependency Array

// BAD: No dependency array = runs after EVERY render
useEffect(() => {
  setCount(count + 1);  // Updates state → re-render → runs again → infinite loop
});

// BAD: Dependency on something that changes every render
useEffect(() => {
  setCount(count + 1);
}, [count]);  // count changes → effect runs → count changes → infinite loop

// GOOD: Empty array = runs once on mount
useEffect(() => {
  fetchData();  // One-time data fetch
}, []);

// GOOD: Correct dependencies
useEffect(() => {
  if (userId) fetchUserData(userId);
}, [userId]);  // Only runs when userId changes

Cause 2: Object or Array in Dependencies (New Reference Each Render)

// BAD: New object created every render = effect runs every render
const options = { page: 1, limit: 10 };  // New reference each render!

useEffect(() => {
  fetchData(options);
}, [options]);  // options always "changed" → infinite loop

// FIX 1: Move object inside the effect (if no dependencies)
useEffect(() => {
  const options = { page: 1, limit: 10 };
  fetchData(options);
}, []);

// FIX 2: useMemo to stabilize the reference
const options = useMemo(() => ({
  page: 1,
  limit: 10
}), []);  // Only creates new object if dependencies change

useEffect(() => {
  fetchData(options);
}, [options]);  // Now stable

// FIX 3: Use primitive values as dependencies
useEffect(() => {
  fetchData({ page, limit });
}, [page, limit]);  // Primitives compare by value, not reference

Cause 3: Function in Dependencies (New Reference Each Render)

// BAD: Function recreated every render
const fetchUser = () => {  // New reference each render!
  return fetch('/api/user');
};

useEffect(() => {
  fetchUser();
}, [fetchUser]);  // fetchUser always "changed" → infinite loop

// FIX: useCallback to stabilize the function reference
const fetchUser = useCallback(() => {
  return fetch('/api/user');
}, []);  // Stable reference

useEffect(() => {
  fetchUser();
}, [fetchUser]);  // Now stable

// FIX 2: Move the function inside the effect (simpler)
useEffect(() => {
  const fetchUser = () => fetch('/api/user');
  fetchUser();
}, []);

Cause 4: setState Inside Effect Without Condition

// BAD: Unconditional state update
useEffect(() => {
  setData(processedData);  // Always updates → re-render → runs again
}, [data]);

// GOOD: Only update if value actually changed
useEffect(() => {
  const processed = processData(data);
  if (processed !== prevProcessed) {  // Check before updating
    setProcessedData(processed);
  }
}, [data]);

// GOOD: Use functional update to avoid stale closures
useEffect(() => {
  setCount(prevCount => prevCount + 1);  // Doesn't need count in deps
}, [someOtherDep]);

Cause 5: Async in useEffect

// BAD: async directly in useEffect
useEffect(async () => {  // Don't do this
  const data = await fetchData();
  setData(data);
}, []);

// GOOD: Define async function inside
useEffect(() => {
  let cancelled = false;

  const fetchAsync = async () => {
    try {
      const data = await fetchData();
      if (!cancelled) setData(data);  // Prevent state update after unmount
    } catch (err) {
      if (!cancelled) setError(err);
    }
  };

  fetchAsync();

  return () => { cancelled = true; };  // Cleanup on unmount
}, []);

Debugging Infinite Loops

// Add this to find which dependency is changing:
useEffect(() => {
  console.log('Effect ran');
}, [dep1, dep2, dep3]);

// Better: Use a custom hook to log changes
function useWhyDidYouUpdate(deps) {
  const prevDepsRef = useRef(deps);
  useEffect(() => {
    const changes = deps.reduce((acc, dep, i) => {
      if (dep !== prevDepsRef.current[i]) {
        acc.push({ index: i, from: prevDepsRef.current[i], to: dep });
      }
      return acc;
    }, []);
    if (changes.length) console.log('Dependency changes:', changes);
    prevDepsRef.current = deps;
  });
}

React 19 Note (2026)

React 19 introduced the React Compiler (formerly React Forget) which automatically memoizes components and values. If you’re on React 19+ with the compiler enabled, many of these issues are handled automatically. However, understanding the underlying cause is still essential for debugging.

Quick Fixes Summary

Cause Fix
Object/array dependency Use primitive deps or useMemo
Function dependency useCallback or move inside effect
State update in effect Add condition before setting state
Missing cleanup Return cleanup function from effect
Async race condition Add cancelled flag, return cleanup

✍️ Leave a Comment

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

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা