Event listeners
The pattern
Register DOM event listeners via useEventListener from @pyreon/hooks. It handles cleanup on unmount, SSR safety, and listener re-binding when the target changes:
import { useEventListener } from '@pyreon/hooks'
function KeyboardShortcuts() {
useEventListener(window, 'keydown', (e) => {
if (e.key === 'Escape') closeModal()
})
return null
}Targets can be:
window— page-level keybinds, resize/scroll, online/offlinedocument— delegation, focus managementAn element signal / ref —
useEventListener(() => buttonEl, 'click', handler)A ref callback — wire via
ref={(el) => buttonEl = el}then pass() => buttonEl
The listener runs on mount, is removed on unmount, and is rebound if the target signal changes.
Why
Raw addEventListener in a component body has three failure modes:
No cleanup — the listener leaks on unmount, fires after the component is gone.
SSR crash —
windowis undefined on the server; the component render fails the whole page.Stale closures — the handler captures signal values at setup time, not at call time, unless you read inside the handler body.
useEventListener solves all three at once. Don't reach for the raw API inside component code.
Anti-pattern
// BROKEN — leaks on unmount, crashes on SSR
function KeyboardShortcuts() {
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal()
})
return null
}// LESS BROKEN — has cleanup, but duplicates what useEventListener does
function KeyboardShortcuts() {
onMount(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeModal()
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
return null
}The onMount version is technically correct. It just reimplements what useEventListener does in 3 lines. Reach for the hook first.
Exceptions
Framework-host chains like view.dom.ownerDocument.addEventListener(...) in CodeMirror plugins are intentional and safe — the host view's own document is accessed through a scoped path, not through a bare document global. The raw-add-event-listener detector recognises these cases and does not flag them.
Related
Detector:
raw-add-event-listener/raw-remove-event-listener— the MCPvalidatetool flags raw listener registrationsReference API:
useEventListenerin@pyreon/hooks— seeget_api({ package: "hooks", symbol: "useEventListener" })Anti-pattern: "Raw addEventListener / removeEventListener in component or hook bodies"