Christopher Alphonse
HomeBlogProjectsAboutResumeLLM Guide
Content
BlogRSS
Profile
AboutResumeProjects
Site
HomeSitemap

© 2022–2026 Christopher Alphonse

  1. Home
  2. Blog
  3. Mastering-side-effects-in-modern-react-a-2025-guide

Christopher Alphonse 's image

Christopher Alphonse

/Christopheralphonse
4 min(s) read
204
views
6/05/26

React useEffect Hook: Common Mistakes and Best Practices

Stop breaking your React components with broken side effects. Learn the most common useEffect mistakes and how to fix them with practical, production-tested patterns.

a diagram of react components including pure code side effect event handler and effects

React useEffect is a React Hook that lets you synchronize a component with an external system — API calls, subscriptions, DOM events, or timers. It replaces the lifecycle methods componentDidMount, componentDidUpdate, and componentWillUnmount from class components, but its behavior surprises developers daily.

Last updated: June 2026

The Most Common React useEffect Mistakes¶

Mistake 1: Missing Dependency Arrays¶

Any value from the component scope (props, state, functions) used inside useEffect must be in the dependency array or the effect captures a stale reference.

javascript
// Bad: count is stale — always logs 0 useEffect(() => { console.log(count); }, []);

Why it breaks: The effect closure captures count at render time. Without count in the dependency array, the closure never updates, causing unpredictable behavior and stale UI state.

Fix: Include all reactive values in the dependency array.

javascript
useEffect(() => { console.log(count); }, [count]);

Mistake 2: Infinite Re-render Loops¶

Updating state inside useEffect without proper guards creates an infinite loop: effect runs, state updates, component re-renders, effect runs again.

javascript
// Bad: infinite loop — every state update triggers re-render, which triggers effect useEffect(() => { setCount(count + 1); }, [count]);

Why it breaks: State updates trigger re-renders, which re-run the effect, which updates state again.

Fix: Use functional updates or move the logic outside useEffect.

javascript
useEffect(() => { setCount(prev => prev + 1); }, []);

Mistake 3: No Cleanup for Side Effects¶

Timers, subscriptions, and AbortControllers left behind after unmount cause memory leaks and update-state-on-unmounted-component warnings.

javascript
// Bad: interval keeps running after component unmounts useEffect(() => { const id = setInterval(() => setTime(Date.now()), 1000); }, []);

Why it breaks: The interval continues firing even after the component is removed from the DOM, attempting to update state on an unmounted component and leaking memory.

Fix: Return a cleanup function.

javascript
useEffect(() => { const id = setInterval(() => setTime(Date.now()), 1000); return () => clearInterval(id); }, []);

Async Functions in useEffect¶

React useEffect does not support async functions directly because async functions return a Promise, not a cleanup function or undefined.

javascript
// Bad: ESLint will warn — async effect returns a Promise useEffect(async () => { const data = await fetchData(); setData(data); }, []);

Fix: Define the async function inside the effect and call it.

javascript
useEffect(() => { const controller = new AbortController(); async function load() { const data = await fetchData({ signal: controller.signal }); if (!controller.signal.aborted) setData(data); } load(); return () => controller.abort(); }, []);

The AbortController pattern prevents state updates after unmount — a critical practice for production React apps.

Object and Function Dependencies¶

Objects and functions create new references on every render, causing effects to re-run on every render cycle.

javascript
// Bad: `options` is a new object every render — infinite loop function Component({ items }) { const options = { format: 'json' }; useEffect(() => { fetch('/api', options); }, [options]); }

Fix: Memoize with useMemo / useCallback or restructure your data to use primitive values.

javascript
const options = useMemo(() => ({ format: 'json' }), []); useEffect(() => { fetch('/api', options); }, [options]);

One Effect per Concern¶

Multiple unrelated side effects in a single useEffect make the dependency array harder to reason about and the cleanup function more complex.

javascript
// Bad: mixing analytics and data fetching in one effect useEffect(() => { trackPageView(pageId); fetchData(); }, [pageId]);

Fix: Split into separate useEffect calls.

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

Build Better Apps With Reinvoice¶

Running a SaaS? Reinvoice handles invoicing, subscription management, and billing automations so you can focus on code. Start your free trial .

Frequently Asked Questions¶

Is useEffect called after every render?

Yes, by default useEffect runs after every completed render. The dependency array controls when it re-runs. An empty array [] means run once after mount.

Can useEffect be async?

Not directly. Define an async function inside the effect and call it, or use an IIFE pattern.

How do I fix the missing dependency warning?

Add the missing value to the dependency array. If you intentionally want to run the effect only on mount, verify the value is truly stable or use the eslint-disable comment with a justification.

Why does useEffect run twice in development?

React 18 Strict Mode intentionally double-invokes effects in development to surface missing cleanup functions. It does not happen in production builds.

When should I use cleanup in useEffect?

Always clean up subscriptions, timers, AbortControllers, and event listeners. Effects that only synchronize props to local state typically do not need cleanup.

Table of Contents

  • The Most Common React useEffect Mistakes
  • Mistake 1: Missing Dependency Arrays
  • Mistake 2: Infinite Re-render Loops
  • Mistake 3: No Cleanup for Side Effects
  • Async Functions in useEffect
  • Object and Function Dependencies
  • One Effect per Concern
  • Build Better Apps With Reinvoice
  • Frequently Asked Questions

Tags

    JavaScript
    React
    TypeScript

Share Post

Sponsored

Try Reinvoice

Send professional invoices in seconds.

Start free trial →

Featured

Systems

Front-End Complexity: Why Simple UI Tasks Take 10x Longer

Why simple front-end tasks take 10x longer. Every UI element is a distributed sy...

machine hallucinations

Why Claude's Hallucinations Made Me a Better Developer

AI hallucinations feel like bugs, but they are actually a forcing function for b...