TL;DR
- Hooks are just an API layered on top of React’s render model, so I use them with that model in mind.
useEffectis for synchronizing with external systems, not as a general “after render” dump bucket.- Most hook bugs are dependency bugs, stale closures, or state-ownership mistakes.
useMemo,useCallback, anduseRefare powerful only when I understand exactly what stability or mutability they are buying me.
Context
Hooks made React code cleaner for me, but they also made it easier to hide complexity in function bodies. I keep the mental model simple: state lives with renders, refs persist without re-rendering, and effects connect React to the outside world.
The Approach
I usually split hooks into four buckets:
- state:
useState,useReducer - refs:
useRef - effects:
useEffect,useLayoutEffect - memoization:
useMemo,useCallback
The stale closure bug is the one I see most often:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, [])
The cleaner fix is:
useEffect(() => {
const id = setInterval(() => {
setCount((current) => current + 1)
}, 1000)
return () => clearInterval(id)
}, [])
I also try to avoid effects when a value can be derived during render:
const filtered = items.filter((item) => item.active)
That is simpler than storing filtered in state and synchronizing it with an effect.
I also keep three hook internals in mind:
- hooks are stored by call order, which is why they cannot live inside conditionals
useLayoutEffectruns after DOM mutation but before paint, so I reserve it for measurement or flicker preventionuseRefis my “latest value” tool when I need mutable current state without triggering another render
Trade-offs
- Hooks make component logic composable, but they also make hidden dependencies easier to introduce.
- Effects are necessary for subscriptions, timers, and network coordination, but overusing them makes components harder to reason about.
useLayoutEffectis useful, but it blocks paint and should stay rare.- Memoization can reduce work, but I only keep it when it clearly protects something expensive or stabilizes an API boundary.