Using unnecessary dependencies in useEffect

Loading

A common React performance issue occurs when useEffect dependencies are incorrectly specified, leading to:

  • Excessive effect executions
  • Infinite loops
  • Unnecessary re-renders

Core Principles

1. The Dependency Array Should Reflect All Reactive Values

Every value used inside the effect that can change between renders must be included in the dependencies array.

// Correct
useEffect(() => {
  document.title = `Hello ${name}`;
}, [name]); // name is correctly included

2. Empty Dependency Array Means “Run Once”

useEffect(() => {
  // Runs only once on mount
  initializeSomething();
}, []); // Empty array = no dependencies

Common Problems and Solutions

Problem 1: Missing Dependencies

// ❌ Dangerous - might miss updates
useEffect(() => {
  fetchData(userId);
}, []); // Missing userId dependency

Solution:

// ✅ Correct
useEffect(() => {
  fetchData(userId);
}, [userId]);

Problem 2: Unnecessary Dependencies Causing Excessive Runs

// ❌ Runs on every render because object reference changes
useEffect(() => {
  doSomethingWith(config);
}, [config]); // config object changes reference often

Solutions:

Option 1: Memoize the dependency

const memoizedConfig = useMemo(() => config, [config.key1, config.key2]);

useEffect(() => {
  doSomethingWith(memoizedConfig);
}, [memoizedConfig]);

Option 2: Use primitive values

useEffect(() => {
  doSomethingWith(config.value);
}, [config.value]); // Only depends on primitive

Problem 3: Functions as Dependencies

// ❌ Will re-run if handleSubmit reference changes
useEffect(() => {
  window.addEventListener('submit', handleSubmit);
  return () => window.removeEventListener('submit', handleSubmit);
}, [handleSubmit]);

Solutions:

Option 1: Move function inside effect (if possible)

useEffect(() => {
  const handleSubmit = () => { /* ... */ };
  window.addEventListener('submit', handleSubmit);
  return () => window.removeEventListener('submit', handleSubmit);
}, []); // No dependencies needed

Option 2: Memoize the function

const handleSubmit = useCallback(() => {
  /* ... */
}, [dependency1, dependency2]);

useEffect(() => {
  window.addEventListener('submit', handleSubmit);
  return () => window.removeEventListener('submit', handleSubmit);
}, [handleSubmit]);

Problem 4: Infinite Loops

// ❌ Infinite loop - updates state then re-runs effect
useEffect(() => {
  setCount(count + 1);
}, [count]);

Solutions:

Option 1: Use functional updates

useEffect(() => {
  setCount(prev => prev + 1);
}, []); // No count dependency needed

Option 2: Add conditional logic

useEffect(() => {
  if (count < MAX_COUNT) {
    setCount(count + 1);
  }
}, [count]);

Advanced Patterns

1. Effect Dependencies That Shouldn’t Trigger

useEffect(() => {
  // Uses currentRef.current but shouldn't trigger effect
  console.log(currentRef.current);
}, [/* what to put here? */]);

Solution: Omit refs from dependencies

useEffect(() => {
  console.log(currentRef.current);
}, []); // Refs are mutable and shouldn't trigger effects

2. Using Previous Values

useEffect(() => {
  if (value !== prevValue) {
    // Do something
  }
}, [value, prevValue]);

Better Solution: Use a custom hook

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

function Component({ value }) {
  const prevValue = usePrevious(value);
  useEffect(() => {
    if (value !== prevValue) {
      // Do something
    }
  }, [value]); // Only need current value in dependencies
}

Best Practices

  1. Use the ESLint plugineslint-plugin-react-hooks will warn about missing dependencies
  2. Separate concerns – Use multiple effects instead of one big effect
  3. Consider custom hooks – For complex effect logic that might be reused
  4. Clean up properly – Always return a cleanup function when needed
  5. Think about effect timing – Effects run after render, consider if you really need an effect

When to Break the Rules

In rare cases, you might intentionally omit dependencies:

  • When you’re absolutely sure the effect should only run once
  • When dealing with non-reactive values (like refs)
  • When using functions that are truly constant (but consider useCallback instead)

Leave a Reply

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