Stable effect dependencies
Depend on primitives, read callbacks via useEffectEvent, init once.
React docsreact.dev
Reference: Initializing the application
From vercel-react-best-practices: rerender-dependencies and 4 more.
Listener churn on every render
Dispatch. Both should log the current count.
count
Handler in depsBad
addEventListener- 0
removeEventListener- 0
- Handler saw
- –
useEffect(subscribe, [event, handler])useEffectEventGoodaddEventListener- 0
removeEventListener- 0
- Handler saw
- –
useEffect(subscribe, [event])function useWindowEvent(event, handler) {
const onEvent = useEffectEvent(handler) // React 19.2+: always the latest handler
useEffect(() => {
const listener = (e) => onEvent(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event]) // not onEvent: it is a new function every render
}
// Older React: the same idea with a ref
function useWindowEvent(event, handler) {
const handlerRef = useRef(handler)
useEffect(() => { handlerRef.current = handler }, [handler])
useEffect(() => {
const listener = (e) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
Which edit reconnects?
Edit the name, re-render, then switch user.
user.id
[user]Bad- Connections opened
- 0
- Same-id reconnects
- 0
useEffect(() => connect(user.id), [user])[user.id]Good- Connections opened
- 0
- Same-id reconnects
- 0
useEffect(() => connect(user.id), [user.id])function Presence({ user }) {
useEffect(() => {
const channel = connect(user.id)
return () => channel.close()
}, [user.id]) // a new object with the same id is not a new user
}
App init: once per load
Remount a few times, with and without Strict Mode.
useEffect(init, [])Bad<App />
checkAuthToken()- 0
- Mounts
- 0
useEffect(() => checkAuthToken(), [])Module-level
didInitGood<App />
checkAuthToken()- 0
- Mounts
- 0
if (didInit) returnlet didInit = false // module scope: one per page load
function App() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
}
// Or run it in the entry module, before anything renders:
if (typeof window !== 'undefined') {
loadFromStorage()
checkAuthToken()
}
Why it matters, pros and cons
Why it matters
A dependency array is a restart list. New objects each render restart everything.
Pros
- One subscription per real change.
- Callback stays current, no stale closure.
- Init runs once, same in dev and prod.
Cons
- A value that should restart the effect belongs in deps.
useEffectEventneeds React 19.2+ and stays inside effects.- A module flag outlives sign-out and tests; reset it.