Hooks
useEventListener
A type-safe window/document/element event listener hook with a latest-ref handler, so it never stale-closures and never rebinds on handler change.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-event-listener.jsonPrompt
Build a React + TypeScript "useEventListener" hook (no dependencies beyond
React; wraps the native DOM addEventListener/removeEventListener).
Contract
- Three overloads, discriminated by `options.target`:
- `useEventListener<K extends keyof WindowEventMap>(eventName: K, handler:
(event: WindowEventMap[K]) => void, options?: { target?: Window; passive?:
boolean; capture?: boolean }): void` — target omitted defaults to `window`.
- `useEventListener<K extends keyof DocumentEventMap>(eventName: K, handler:
(event: DocumentEventMap[K]) => void, options: { target: Document;
passive?: boolean; capture?: boolean }): void`.
- `useEventListener<K extends keyof HTMLElementEventMap, T extends
HTMLElement>(eventName: K, handler: (event: HTMLElementEventMap[K]) =>
void, options: { target: React.RefObject<T | null>; passive?: boolean;
capture?: boolean }): void`.
- `eventName` and the `event` argument's type are inferred from whichever
event map matches the resolved `target` — no manual casts at the call site.
Behavior
- The listener attaches inside a `useEffect` whose dependency array is only
`[eventName, target, passive, capture]` — deliberately excluding `handler`
and excluding the `options` object itself, so a fresh inline `{ target,
passive }` object literal passed on every render does not cause a rebind.
- `handler` is written into a ref on every render, via a second, unconditional
effect with no dependency array. The listener actually registered with the
DOM is a stable wrapper that always calls `handlerRef.current(event)`, so
the consumer's latest closure runs on every event without the attach/detach
effect ever re-executing.
- On unmount, or whenever `eventName`/`target`/`passive`/`capture` change, the
previous listener is removed (matching `capture`) before a new one attaches.
- If `target` is a `RefObject` whose `.current` is `null` when the effect
runs, no listener is attached and nothing throws — and it does NOT
auto-attach later: the ref OBJECT identity never changes, so a target
that mounts afterwards (conditional rendering) is never picked up. For
late-mounted targets, remount the consumer with a `key`, or reach for a
callback-ref-based hook (`use-hover` shows that pattern). Targets that
exist for the component's whole life — window, document, an
always-rendered element — are the intended fit.
- The hook body never reads `window`/`document` during render — only inside
effects — so it's safe to call from a component that also renders on the
server; the listener simply attaches once the effect runs client-side.
Rendering & styling
- The hook renders nothing and owns no DOM node — there is no markup or
token to style here. Consumers own all UI built on top of the events it
reports.
Customization levers
- Add an `AbortSignal`-based variant (`options.signal`) so one signal can tear
down many listeners at once instead of relying on unmount alone.
- Extend `eventName` to accept an array of event names sharing one handler
(e.g. `["mousedown", "touchstart"]` for an outside-click hook) when a call
site needs more than one event wired to the same callback.Concepts
- Latest-ref handler —
handleris synced into a ref every render, so the DOM listener (a stable wrapper function) always calls the freshest closure. This is what lets a handler read current props/state without ever going stale, without the listener itself needing to change. - Zero-rebind listener — the attach/detach effect depends only on
eventName/target/passive/capture, never onhandleror theoptionsobject's identity. A parent re-rendering with a brand-new inlinehandler(or a brand-new{ target, passive }object literal) does not tear down and reattach the native listener. - Target-discriminated event map — three overloads narrow
eventNameand theeventargument's type toWindowEventMap/DocumentEventMap/HTMLElementEventMapbased onoptions.target, soeventcomes back correctly typed (e.g.MouseEvent,KeyboardEvent) with no manual cast at the call site. - SSR-safe effect body —
window/documentare only ever read insideuseEffect, never during render, so the hook needs notypeof windowguard yet is safe to include in a server-rendered tree.