useEventListener
A hook that attaches a typed event listener with automatic cleanup.
Last key on window: —
Installation
Usage
addEventListener in an effect is four lines of boilerplate that go wrong in two ways: the cleanup
gets forgotten, or the handler is added to the dependency array and the listener is torn down and
re-attached on every render. This hook fixes both, and keeps the event fully typed.
import { useEventListener } from "@/hooks/use-event-listener";
function ShortcutBar() {
useEventListener("keydown", (event) => {
if (event.key === "Escape") close();
});
return null;
}With no target, the listener goes on window — event is narrowed from WindowEventMap, so
event.key type-checks and event.clientX does not.
Targets
Pass a ref or an element as the third argument. The ref does not need to be populated on the first render; the listener is attached in an effect.
const cardRef = React.useRef<HTMLDivElement>(null);
useEventListener("pointermove", (event) => setPoint({ x: event.clientX, y: event.clientY }), cardRef);
useEventListener("visibilitychange", () => setActive(!document.hidden), document);The event type follows the target: HTMLElementEventMap for elements, DocumentEventMap for
document, WindowEventMap for the default.
Options
| Option | Description |
|---|---|
capture | Listen during the capture phase. |
passive | Promise never to call preventDefault — required for smooth touchmove/wheel. |
once | Remove the listener after it fires once. |
enabled | Detach without unmounting. Flip it instead of conditionally calling the hook. |
Behavior
- The handler is read from a ref, so it always sees fresh props and state without re-subscribing. You never need to memoize what you pass in.
- Nothing touches the DOM during render, so the hook is safe in server-rendered trees.
targetresolution happens inside the effect, which is whywindowanddocumentdefaults do not break SSR.