DEV Community

Naeem Amin
Naeem Amin

Posted on Originally published at naeemamin.cloud on

React useEffect Common Mistakes: Fixes & Best Practices

Learn how to avoid the React useEffect common mistakes. Discover real-world pitfalls, better patterns, and practical fixes for efficient React code.

If you're building anything serious with React, you've probably bumped into unexpected behavior from useEffect. Even after years of React work, I see the same useEffect common mistakes crop up in code reviews, onboarding, and even my own experiments.

useEffect is powerful, but it comes with a learning curve; missing small details can create subtle bugs, memory leaks, unnecessary network calls, and performance bottlenecks.

In this comprehensive guide, I'll show you where most developers trip up, how to quickly spot problems, and offer practical code fixes that have worked for my teams. Whether you're a beginner or want a refresher, you'll get a deep understanding of useEffect’s quirks and best practices.

Prerequisites: Know What useEffect Does

Before we look at the common mistakes with useEffect in React, let's make sure everyone's on the same page. useEffect is a React Hook for running side effects in functional components. Side effects include data fetching, state synchronization, subscriptions, timers, manual DOM mutations, logging, and more.

The basic idea: React renders your component, then after the DOM updates, useEffect fires. You control when and how often useEffect fires by specifying dependencies.

Seasoned React devs know: understanding the timing and lifecycle around useEffect is a foundational skill. If you're not sure when or why to use useEffect, check out the official React docs.

  • React 18+ installed
  • Basic understanding of functional components
  • Familiarity with JavaScript closures and promises
  • npm + a project set up with ESLint and prettier (optional, but highly recommended)

Mistake #1: Leaving Out useEffect’s Dependency Array

Leaving the dependency array empty or omitting it altogether is a classic beginner mistake. When the dependency array is omitted, useEffect runs after every render, not just on mount or when dependencies change. This is rarely what you want—especially if your effect involves data fetching, event listeners, or state changes.

A missing dependency array can lead to expensive network calls, performance issues, and even infinite update loops if your effect mutates state.

useEffect(() => {
  fetchUserData(); // Runs after every render! ⚠️
});
Enter fullscreen mode Exit fullscreen mode

How to Fix: Use the Dependency Array Wisely

Always pass the dependency array as the second argument to useEffect.

  • If you want the effect to run only once on mount (like componentDidMount), use an empty array: []. - If it depends on specific props or state, list them explicitly in the array. This makes your intentions clear, helps React prevent stale closures, and works well with linters.

A correct dependency array also helps React Batch state updates efficiently in React 18’s concurrent rendering mode.

useEffect(() => {
  fetchUserData();
}, []); // Only runs once on mount

useEffect(() => {
  fetchUserData(userId);
}, [userId]); // Runs when userId changes
Enter fullscreen mode Exit fullscreen mode

Mistake #2: Forgetting to List All Dependencies

React's linter (eslint-plugin-react-hooks) is your best friend here—it warns if you forget items in the dependency array, but it’s easy to ignore those warnings or misunderstand them in complex situations.

Leaving out a dependency means your effect can use stale data, leading to tough bugs that only surface under certain conditions. This mistake is particularly common when variables, functions, or context values change outside the effect's visible code.

  • Accidentally only listing some variables
  • Referencing functions or props but not including them
  • Relying on external mutation (which React doesn't track)
useEffect(() => {
  apiCall(filter); // filter used, but not listed below ❌
}, []);
Enter fullscreen mode Exit fullscreen mode

How to Fix: Always Include Dependencies

Every value from the component scope used inside the effect should appear in the dependency array. Don’t fight the linter! If it suggests a variable, include it to avoid using outdated values.

If you must avoid a dependency (which should be rare), consider wrapping non-stable functions or values in useCallback or useMemo, or storing them in a ref. But always ask: are you breaking a recommended pattern? Most useEffect bugs come from missing dependencies.

useEffect(() => {
  apiCall(filter);
}, [filter]); // Correct: filter included
Enter fullscreen mode Exit fullscreen mode

Mistake #3: Creating Functions or Objects Inside useEffect

A subtle react useEffect common mistake is defining functions, objects, or arrays inline in your component, and then using them as dependencies.

Because functions/objects are recreated on every render, their references change and React sees them as new, so useEffect runs again. This can create unwanted loops or constant re-fetching, especially with objects or functions derived from props or state.

const fetchData = () => {/* ... */};
useEffect(() => {
  fetchData();
}, [fetchData]); // fetchData is 'new' on every render
Enter fullscreen mode Exit fullscreen mode

How to Fix: Memoize or Move Functions Outside

If you need to reference a function or object in the dependency array, ensure its reference is stable. There are three typical solutions:

  • Wrap the function in useCallback (dependencies included appropriately) - Wrap objects or arrays in useMemo - Define functions/objects outside the component body if they don't depend on props/state

This prevents unnecessary re-runs of useEffect, especially in lists or performance-sensitive areas.

const fetchData = useCallback(() => {
  // ...fetch logic that depends on userId, etc
}, [userId]);

useEffect(() => {
  fetchData();
}, [fetchData]);
Enter fullscreen mode Exit fullscreen mode

Advanced Note: Custom Hooks and External Functions

When using external utility functions or custom hooks within useEffect, always check if they reference props, state, or variable values. If they do, useCallback/useMemo may be required to memoize those custom hooks or functions' results. This is critical when working on shared business logic or libraries, as hidden dependencies may cause subtle bugs and violate the rules of hooks. Refer to the React Hooks FAQ.

Mistake #4: Missing useEffect Cleanup

Effects that subscribe to events, set timers, or connect to external APIs should always provide cleanup. Otherwise, you’ll accumulate event listeners, timers, or subscriptions every time the component re-renders or unmounts. In larger apps, this can cause memory leaks, duplicate event handling, and subtle app-wide bugs. This is one of the most common useEffect cleanup issues in React.

useEffect(() => {
  window.addEventListener('resize', onResize);
  // No cleanup: event listener stays forever ❌
});
Enter fullscreen mode Exit fullscreen mode

How to Fix: Always Return a Cleanup Function

useEffect lets you return a cleanup function, which React runs before the next effect fires and when the component unmounts. Always clean up subscriptions, event listeners, and timers. For async logic (like observables or WebSockets), also cancel or unsubscribe in cleanup. This helps avoid memory leaks and ensures no background actions remain after unmount.

useEffect(() => {
  window.addEventListener('resize', onResize);
  return () => {
    window.removeEventListener('resize', onResize);
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Advanced Note: Stale Closures in Cleanup

If your cleanup function references variables that may change (like handlers or props), ensure those variables are included in the dependency array or useRef to store the latest value. Otherwise, your cleanup could behave unexpectedly and fail to detach the right handler.

Mistake #5: Async Functions Directly in useEffect

You can’t make the effect function itself async because it would return a Promise (not a cleanup function, as React expects). Doing so can cause React to ignore the cleanup or throw confusing errors and warnings, especially in concurrent mode.

useEffect(async () => { // ❌ Invalid - don't do this
  await fetch('/api/stuff');
}, []);
Enter fullscreen mode Exit fullscreen mode

How to Fix: Define Async Functions Inside useEffect

To use async logic in useEffect, define an async function inside the effect and call it synchronously. Handle error cases and potential unmounted component warnings (for example, by using an abort controller or a 'mounted' boolean guard for cleanup).

Be careful about updating state after an async operation; if the component unmounts before the data arrives, you may update state on an unmounted component unless you clean up properly.

useEffect(() => {
  let isActive = true;
  async function fetchStuff() {
    try {
      const res = await fetch('/api/stuff');
      if (isActive) {
        // do something with res
      }
    } catch (error) {
      // handle error
    }
  }
  fetchStuff();
  return () => {
    isActive = false;
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

Mistake #6: State Updates Triggering useEffect Loops

Another common useeffect infinite loop in react: You set state inside useEffect using the same value present in the dependency array. Since updating state triggers a new render, which in turn triggers the effect again (because the value changed), you quickly end up in an infinite loop.

This is a typical pitfall for beginners and often happens when trying to increment a counter or fetch with a dependency tied to updated state.

  • Updating a value that the effect depends on
  • useEffect keeps running forever
  • React warns about 'Maximum update depth exceeded'
useEffect(() => {
  setCount(count + 1);
}, [count]); // Infinite loop!
Enter fullscreen mode Exit fullscreen mode

How to Fix: Separate State and useEffect Dependencies

Usually, you don’t need to include variables you change inside the effect in the dependency array. Alternatively, add a guard clause or condition to prevent endless looping. Always use React's built-in linter to analyze your dependencies and check reducer-based state for more complex flows.

For complex use cases, consider useRef to store mutable variables that don't trigger re-renders, breaking the update loop.

useEffect(() => {
  if (count < 5) {
    setCount(count + 1);
  }
}, [count]);
Enter fullscreen mode Exit fullscreen mode

Best Practices: Making useEffect Predictable

Here’s a summary of my useEffect mental checklist for every project:

  • List all values used in your effect in the dependency array
  • Prefer splitting effects by logical purpose instead of cramming too much logic into one useEffect
  • Keep effects focused—each one should do one thing (fetch, subscribe, animate, etc.)
  • Use useCallback/useMemo for stable dependencies (functions, objects, arrays)
  • Only use an empty dependency array if you really mean 'run once on mount'
  • Heed linter warnings—they are right 99% of the time, and catching mistakes early is easier than debugging stale bugs later
  • Document your useEffect: a comment on why an effect runs once or depends on certain variables helps future devs

Advanced useEffect gotchas in React 18+

With React 18’s concurrent rendering, effects may mount/umount multiple times in development for non-determinism detection. Your cleanup logic must be robust—never assume an event listener/timer/etc. has only one add/remove cycle.

Also, popular data-fetching libraries like react-query or SWR offer alternatives to traditional useEffect-based data fetching and may reduce the need for manual effects management. Consider these for large or async-heavy codebases to avoid common useEffect traps.

Further Reading

Conclusion: Master useEffect by Avoiding These Pitfalls

Getting comfortable with useEffect unlocks powerful patterns in React. The most common problems I’ve diagnosed across teams almost always trace back to one of these mistakes. Now that you know the core react useeffect common mistakes—use them as a review checklist during development and code reviews. The practical fixes above will help make your code cleaner, more reliable, and simpler to reason about.

By combining React’s powerful abstraction with robust tools like ESLint and custom hooks, you can avoid subtle bugs and keep your codebase stable as it scales. Your future self (and team) will thank you for mastering these useEffect essentials.

Frequently asked questions

Why does my useEffect run infinitely?

An infinite useEffect loop usually happens because a value in your dependency array changes every render, or a state update inside useEffect itself triggers another render. Always review what you include in your dependency array, avoid updating state directly tied to dependencies, and watch out for objects/functions recreated on every render. Linter warnings for dependency arrays will help catch these cases quickly.

Do I always need to include every variable in the dependency array?

It's best practice to include all values used inside useEffect in the dependency array, unless you know exactly why you want to use stale values (which is very rare). Linters like eslint-plugin-react-hooks are designed to help spot missing dependencies and keep your dependencies honest. Memoize values/functions if you don't want them to trigger the effect.

How do I use async/await inside useEffect?

Define an async function inside your useEffect, and then call it. The effect function itself should not be async, as React expects either nothing or a cleanup function. If your async logic fetches data, ensure you handle component unmounts to avoid updating state after unmount (using a 'mounted' variable or an abort controller).

What's the best way to clean up event listeners in useEffect?

Return a cleanup function from your useEffect that removes the event listener or unsubscribe logic. Cleanup runs when the component unmounts and before the effect re-runs on dependency change, thus preventing memory leaks and redundant subscriptions. For advanced cleanup, make sure your reference (handler) is stable or useRef for the current function.

What tools help avoid useEffect mistakes?

Use eslint-plugin-react-hooks in your project—it will warn about missing dependencies and common misuse of useEffect. For larger-scale or deeply async codebases, consider data fetching libraries (react-query, SWR) to abstract away many common pitfalls. Also try Prettier to keep your hooks formatting consistent.

Originally published at naeemamin.cloud.

Top comments (0)