title: Store Event Handlers in Refs impact: LOW impactDescription: stable subscriptions
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
Incorrect (re-subscribes on every render):
function useWindowEvent(event: string, handler: () => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
}
Correct (stable subscription):
function useWindowEvent(event: string, handler: () => void) {
const handlerRef = useRef(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const listener = () => handlerRef.current()
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
}
Alternative: use useEffectEvent if you're on latest React:
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
useEffectEvent provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.