Mastering React useEffect: The Complete Guide to Avoid Common Pitfalls

This guide explains what useEffect does, its three core execution rules, how dependency arrays change its behavior, eight practical scenarios, six typical pitfalls with concrete code examples, and best‑practice recommendations for writing clean, leak‑free React functional components.

CodeNotes
CodeNotes
CodeNotes
Mastering React useEffect: The Complete Guide to Avoid Common Pitfalls

1. What is useEffect?

useEffect is the Hook used in React function components to handle side effects such as data fetching, timers, subscriptions, DOM manipulation, and local storage.

API requests, data fetching

Timers, delays

Event subscriptions, DOM listeners

Manual DOM operations

Local storage read/write

The core execution rules are:

Runs after the component mounts for the first time.

Runs when any value in the dependency array changes.

Runs the cleanup function before the component unmounts or before re‑execution.

useEffect(() => {
  // side‑effect logic
  return () => {
    // cleanup logic (runs on unmount / before re‑run)
  };
}, [dependencyArray]);

2. Dependency array determines behavior

1. No dependency array – runs on every render

useEffect(() => {
  console.log("Component renders each time");
});

Rarely needed; can cause performance waste.

2. Empty array – runs once (componentDidMount)

useEffect(() => {
  console.log("Runs only on first render");
}, []);

Typical for initial data requests or one‑time configuration.

3. With dependencies – runs when they change

useEffect(() => {
  console.log("Runs when userId changes");
}, [userId]);

Common for refetching on parameter changes, linking logic, or watching state.

3. Common useEffect scenarios

Scenario 1: Initial data fetch

useEffect(() => {
  // async request
  const fetchData = async () => {
    const res = await fetch("/api/list");
    const data = await res.json();
    setList(data);
  };
  fetchData();
}, []);

Scenario 2: Refetch when a parameter changes

useEffect(() => {
  fetchListByKeyword(keyword);
}, [keyword]);

Scenario 3: Timer / interval with cleanup

useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

Scenario 4: DOM manipulation / third‑party library init

useEffect(() => {
  const dom = document.getElementById("chart");
  const myChart = echarts.init(dom);
  myChart.setOption(option);
  return () => myChart.dispose();
}, [option]);

Scenario 5: Global event listeners

useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []);

Scenario 6: Sync props to local state

useEffect(() => {
  setLocalData(props.data);
}, [props.data]);

Scenario 7: Cleanup to prevent memory leaks

useEffect(() => {
  const controller = new AbortController();
  const fetchData = async () => {
    try {
      const res = await fetch("/api/data", { signal: controller.signal });
      const data = await res.json();
      setData(data);
    } catch (err) {
      console.log("Request cancelled");
    }
  };
  fetchData();
  return () => controller.abort();
}, []);

Scenario 8: Full lifecycle mapping

Mount: useEffect(() => {}, []) Cleanup: return a cleanup function

Update:

useEffect(() => {}, [deps])

4. Six frequent pitfalls

Pitfall 1: Missing dependencies

// Wrong – keyword omitted from deps
const [keyword, setKeyword] = useState("");
useEffect(() => {
  search(keyword);
}, []); // never updates search

Solution: include every variable used inside the effect in the dependency array.

Pitfall 2: Infinite loop

Reference types in deps cause new identity each render.

Updating state inside the effect triggers a re‑run.

// Infinite loop example
const [list, setList] = useState([]);
useEffect(() => {
  setList([1, 2, 3]); // state update → re‑render → effect runs again
}, [list]);

Solution: use primitive values, memoize objects/arrays with useMemo or useCallback, and avoid putting the state being set into the deps.

Pitfall 3: Not cleaning up – memory leak

Console warning:

Can't perform a React state update on an unmounted component.

Cause: timers, requests, or listeners continue after the component is unmounted and try to update state.

Solution: always provide a cleanup function for every side effect.

Pitfall 4: Stale closure from function defined outside

// Wrong – function defined outside captures old closure
const fetchData = () => {
  console.log(userId);
};
useEffect(() => {
  fetchData();
}, []);

Solution: define the function inside the effect or memoize it with useCallback and add it to the deps.

Pitfall 5: Async function directly as effect callback

// Wrong – async effect returns a Promise
useEffect(async () => {
  const data = await fetchData();
}, []);

Solution: declare an async function inside the effect and invoke it.

Pitfall 6: Hard‑coded dependency arrays

Using [{}] or [[]] creates a new object/array on every render, so the effect never sees a matching dependency and never updates.

5. Best practices

Let each useEffect handle a single concern; avoid mixing unrelated logic.

Always write a cleanup function for timers, requests, listeners, and third‑party instances.

Strictly list all dependencies; do not silence ESLint warnings.

Prefer state‑derived calculations over effects when possible.

For async requests, include loading state, error handling, and an abort controller.

Memoize component functions with useCallback and objects/arrays with useMemo to prevent unnecessary re‑executions.

6. Conclusion

useEffect

essentially watches dependency changes, runs side effects, and cleans up at the appropriate time. The challenge lies in following its rules, understanding execution timing, and habitually cleaning up. Mastering the scenarios and pitfalls described here enables you to write stable, maintainable, and pitfall‑free React code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

FrontendperformanceJavaScriptReActMemory LeakHooksuseEffect
CodeNotes
Written by

CodeNotes

Discuss code and AI, and document daily life and personal growth.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.