The biggest improvement you can make to a React codebase is removing useEffects that shouldn't be there. The React docs ship a whole page titled 'You Might Not Need an Effect.' Read it. Then re-read it.
What useEffect is actually for
useEffect synchronizes React state with something outside React: a DOM API (focus, scroll), a browser API (event listeners, timers, geolocation), a network connection (subscriptions, websockets), a third-party library that doesn't know about React. If the side effect is purely inside React (computing derived state, updating other state), you don't want an effect.
The dependency array
useEffect(fn)— runs after every render. Almost always wrong unless you're deliberately running on every render.useEffect(fn, [])— runs once after mount (and on unmount if a cleanup is returned).useEffect(fn, [a, b])— runs after mount and wheneveraorbchanges by reference equality.
The ESLint rule react-hooks/exhaustive-deps flags missing dependencies. Don't suppress it casually — a missing dep is usually a bug.
The cleanup function
Return a function from the effect to clean up. React calls it before the effect re-runs and on unmount. This is how you remove event listeners, cancel subscriptions, abort fetches.
StrictMode double-invocation
In development, React's <StrictMode> intentionally mounts every component twice. Your mount-only effect runs, cleans up, runs again. This is a tool to surface effects that aren't safely idempotent — if your effect breaks under double-invoke, it has a bug (usually a missing cleanup, or a side effect that should not have been an effect in the first place). Production runs once.
Effects that shouldn't be effects
Five common anti-patterns the React docs explicitly call out:
- Transforming data for rendering → compute in render.
- Caching expensive computations → use
useMemo(or trust the React Compiler in Track 7). - Resetting state when a prop changes → use a
keyon the component. - Adjusting state based on another state change → compute the dependent state during render.
- Notifying parent of a change → call the parent's callback from the event handler, not after the state settles.