← Writing
snippet

A useEventListener hook that doesn't go stale

Attaching an event listener in React without re-binding on every render or capturing a stale callback.

The trap with addEventListener in an effect is the dependency array: put the handler in it and you re-bind on every render; leave it out and you capture a stale closure. Storing the handler in a ref sidesteps both.

import { useEffect, useRef } from "react";
 
export function useEventListener<K extends keyof WindowEventMap>(
	type: K,
	handler: (event: WindowEventMap[K]) => void,
) {
	const savedHandler = useRef(handler);
 
	// Keep the ref current without re-running the listener effect.
	useEffect(() => {
		savedHandler.current = handler;
	}, [handler]);
 
	useEffect(() => {
		const listener = (event: WindowEventMap[K]) => savedHandler.current(event);
		window.addEventListener(type, listener);
		return () => window.removeEventListener(type, listener);
	}, [type]);
}

The listener effect only depends on type, so it binds once. The handler always reads through savedHandler.current, so it's never stale.

#react#hooks#typescript