Avoid These React useEffect Mistakes : Part - 1

·

5 min read

The useEffect hook is essential for handling side effects in React, but it's easy to misuse. Below are some common mistakes developers make and tips on how to avoid them, complete with Do’s and Don’ts.


1. Forgetting the Dependency Array

Don't:

Omitting the dependency array will cause the effect to run after every render.

useEffect(() => {
  // This will run after every render!
  console.log("Effect running");
});

Do:

Include a dependency array to control when the effect should run.

useEffect(() => {
  console.log("Effect running only once");
}, []);  // Runs only on mount and unmount

Why?
Without the dependency array, you risk unnecessary re-renders and performance issues.


2. Including Too Many Dependencies

Don't:

Passing too many dependencies can cause frequent re-renders.

useEffect(() => {
  // This effect depends on many variables
  console.log("Effect running");
}, [var1, var2, var3, var4]);

Do:

Limit dependencies to only the ones that affect the side effect.

useEffect(() => {
  console.log("Effect running");
}, [var1]);  // Include only the necessary dependencies

Why?
Including unnecessary dependencies will make the effect trigger more than required, impacting performance.


3. Mutating State Directly in useEffect

Don't:

Updating state inside the effect without considering async behavior can lead to infinite loops.

useEffect(() => {
  setState(state + 1);  // This causes infinite re-renders!
}, [state]);

Do:

Use state setters wisely, and ensure your effect only triggers when necessary.

useEffect(() => {
  if (state < 10) {
    setState(state + 1);
  }
}, [state]);  // Effect stops running after state reaches 10

Why?
Direct state updates trigger re-renders, which may cause infinite loops if not carefully handled.


4. Missing Cleanup for Side Effects

Don't:

Failing to clean up side effects like subscriptions or timers can cause memory leaks.

useEffect(() => {
  const intervalId = setInterval(() => {
    console.log("Timer");
  }, 1000);

  // Forgot to clean up
}, []);

Do:

Return a cleanup function to stop subscriptions, timers, or listeners when the component unmounts.

useEffect(() => {
  const intervalId = setInterval(() => {
    console.log("Timer");
  }, 1000);

  return () => clearInterval(intervalId);  // Cleanup to avoid memory leaks
}, []);

Why?
Without proper cleanup, you'll leave behind unwanted side effects that can degrade performance.


5. Not Handling Asynchronous Code Correctly

Don't:

Using asynchronous functions directly in useEffect can lead to unexpected behavior.

useEffect(async () => {  // This is invalid syntax
  const data = await fetchData();
  setData(data);
}, []);

Do:

Define an async function inside useEffect and call it.

useEffect(() => {
  const fetchData = async () => {
    const result = await fetch("/api/data");
    setData(result);
  };

  fetchData();  // Call the async function
}, []);

Why?
useEffect itself can't be marked as async, but you can use an inner async function to handle asynchronous logic.


6. Ignoring Race Conditions in Async Effects

Don't:

Allow outdated responses to override newer ones in async requests.

useEffect(() => {
  fetchData().then((data) => setData(data));
}, []);

Do:

Use a flag or a cancellation token to ignore outdated async results.

useEffect(() => {
  let isActive = true;

  const fetchData = async () => {
    const result = await fetch("/api/data");
    if (isActive) {
      setData(result);
    }
  };

  fetchData();

  return () => {
    isActive = false;  // Cleanup on unmount
  };
}, []);

Why?
This prevents race conditions where old async calls might set state after newer ones, causing inconsistent UI updates.


7. Ignoring Dependencies of Functions in the Effect

Don't:

Using a function inside useEffect but forgetting to declare it as a dependency.

const fetchData = () => { /* fetch logic */ };

useEffect(() => {
  fetchData();  // fetchData is used but not in the dependency array!
}, []);

Do:

Include the function in the dependency array if it’s used inside the effect.

useEffect(() => {
  fetchData();
}, [fetchData]);  // Function must be in the dependency array

Why?
Functions used inside useEffect should be declared as dependencies, or you risk stale closures.


8. Using useEffect for Synchronous Side Effects

Don't:

Use useEffect for synchronous tasks like updating the DOM when useLayoutEffect is more appropriate.

useEffect(() => {
  document.title = "Page Title";  // Can cause layout shift
}, []);

Do:

Use useLayoutEffect for synchronous updates that affect the DOM before painting.

useLayoutEffect(() => {
  document.title = "Page Title";  // Updates DOM before paint
}, []);

Why?
useLayoutEffect runs synchronously after DOM mutations but before painting, ensuring smoother UI updates.


Here are more common useEffect mistakes with Do’s and Don’ts, plus examples for your blog post:


9. Triggering Effects Too Frequently

Don't:

Trigger an effect every time a variable changes unnecessarily, especially if the effect has costly computations or network requests.

useEffect(() => {
  expensiveComputation();  // This runs every time the component renders
}, [inputValue]);  // Dependency changes too frequently

Do:

Debounce or throttle expensive operations to avoid excessive triggers.

useEffect(() => {
  const handler = setTimeout(() => {
    expensiveComputation();
  }, 300);  // Debounce expensive calls

  return () => clearTimeout(handler);  // Cleanup on unmount
}, [inputValue]);

Why?
Without throttling or debouncing, you risk slowing down the UI by overloading it with computations or API requests.


For more detailed insights on mastering useEffect, check out Part 2 of this blog.