Stop Using useEffect Like This. You're Doing It Wrong.
I've reviewed 200+ pull requests in the last year. The single most common mistake? Developers treating useEffect like componentDidMount. It was never meant to work that way.
I've been doing code review for two years at the team level, and before that at several companies as a consultant. In that time I've reviewed more than 200 pull requests that touched React components.
The single most common mistake, by a wide margin, is misusing useEffect. And I don't mean missing dependencies — everyone knows about that. I mean something more fundamental: a misunderstanding of what useEffect is for.
The Wrong Mental Model
The class component era burned a mental model into our brains: lifecycle methods. componentDidMount runs once. componentDidUpdate runs when something changes. componentWillUnmount runs on cleanup.
When hooks arrived, most developers translated this directly: useEffect with empty deps is componentDidMount. That model is wrong. It works coincidentally in simple cases and fails in complex ones.
useEffect is for synchronizing React state with something outside React — a DOM node, a third-party library, a WebSocket, a browser API. If you're not crossing that boundary, you probably shouldn't be using useEffect.
Mistake #1: Using useEffect for Derived State
This is the most common. You have some state, and you want another piece of state derived from it:
// ❌ Wrong — causes double render
const [items, setItems] = useState([]);
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter(i => i.active));
}, [items]);
// ✅ Correct — just derive it
const [items, setItems] = useState([]);
const filtered = items.filter(i => i.active);
The wrong version double-renders: items updates → component renders → effect runs → filtered updates → component renders again. You've turned one state update into two renders, with a frame of stale UI in between.
Mistake #2: Fetching Without Cleanup
Data fetching in useEffect is a legitimate use case. But almost every example in the wild is missing the cleanup:
useEffect(() => {
const controller = new AbortController();
fetch(url, {
signal: controller.signal
}).then(setData).catch(() => {});
return () => controller.abort();
}, [url]);
Without the abort controller, if the component unmounts before the fetch completes, you'll try to set state on an unmounted component. In React 18 Strict Mode, your effect runs twice in development — without cleanup, you make two network requests and potentially show stale data from the first one.
The One Question to Ask First
Before you type useEffect, ask: am I synchronizing with something outside React? If yes — a fetch, a WebSocket, a DOM API, a timer — proceed. If no, you almost certainly need either derived state, useMemo, or an event handler.
The React team has a doc called “You Might Not Need an Effect” that I assign as reading to every new engineer on my team. It's one of the best pieces of React documentation ever written. Go read it before you write another effect.
useEffect isn't a lifecycle hook. It's a synchronization primitive. Once that clicks, you'll write dramatically better React code — and you'll find yourself reaching for it far less often.
Built with Passion
© 2026 Built with ❤️ & Code by Nishal Poojary.
The Land of Spirituality and Philosophy
Bangalore · India
Thanks for making it
to the end 🙌🏻
