Infinite loop in useEffect due to incorrect dependencies

Loading

Avoiding Infinite Loops in React’s useEffect

One of the most common React pitfalls is creating infinite loops with useEffect due to incorrect dependency management. These loops can crash your application by causing unlimited re-renders.

Why Infinite Loops Happen

An infinite loop occurs when:

  1. Your useEffect updates state
  2. That state is in its dependency array
  3. The update triggers the effect again
const [count, setCount] = useState(0);

// ❌ Infinite loop
useEffect(() => {
  setCount(count + 1); // Updates count
}, [count]); // count is a dependency

Common Scenarios and Fixes

1. State Updates in useEffect

Problem:

const [data, setData] = useState(null);

useEffect(() => {
  fetchData().then(setData); // Updates data
}, [data]); // ❌ data is dependency

Solution:

useEffect(() => {
  fetchData().then(setData);
}, []); // ✅ Empty array if you only want to run once

2. Object/Array Dependencies

Problem:

const [user, setUser] = useState({ id: 1 });

useEffect(() => {
  updateUser(user);
}, [user]); // ❌ New object reference every render

Solution:

const { id } = user;
useEffect(() => {
  updateUser(user);
}, [id]); // ✅ Only depend on primitive value

3. Function Dependencies

Problem:

const fetchData = () => { /*...*/ };

useEffect(() => {
  fetchData();
}, [fetchData]); // ❌ New function reference every render

Solution:

const fetchData = useCallback(() => {
  /*...*/
}, []); // ✅ Stable function reference

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

Advanced Patterns

1. Conditional State Updates

useEffect(() => {
  if (needsUpdate) {
    setData(computeNewData());
  }
}, [needsUpdate]); // Only run when needsUpdate changes

2. Using Previous State

useEffect(() => {
  setCount(prevCount => prevCount + 1); // Doesn't need count in deps
}, [trigger]); // Only runs when trigger changes

3. Deep Object Comparison

const [config, setConfig] = useState({ theme: 'dark' });

useEffect(() => {
  console.log('Config changed');
}, [JSON.stringify(config)]); // Stringify for deep comparison

Debugging Infinite Loops

  1. Check your dependency array – Are you including state that gets updated?
  2. Look for state updates – Is any state being modified in the effect?
  3. Add console logs – Track effect executions and state changes
  4. Use React DevTools – Inspect component re-renders

Real-World Example

Problematic API Fetching

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

  // ❌ Infinite loop - user changes every fetch
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId, user]);

  return <div>{user?.name}</div>;
}

Corrected Version

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

  // ✅ Only runs when userId changes
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return <div>{user?.name}</div>;
}

Key Takeaways

  1. Never include in dependencies:
  • State that gets updated in the effect
  • Objects/arrays that change reference each render
  1. Use primitive values in dependency arrays when possible
  2. Stabilize references with useCallback and useMemo
  3. Consider if you really need dependencies that cause re-runs
  4. Remember the ESLint plugin will warn about missing dependencies

Proper dependency management prevents infinite loops while ensuring your effects run when needed. Always question whether each dependency is truly necessary for your effect’s logic.

Leave a Reply

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