Event listeners
The pattern
Register DOM event listeners via useEventListener from @pyreon/hooks. It handles cleanup on unmount and SSR safety:
// @check
import { useEventListener } from '@pyreon/hooks'
declare function closeModal(): void
function KeyboardShortcuts() {
useEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal()
})
return null
}Targets can be:
window(default — omit the 4th argument) — page-level keybinds, resize/scroll, online/offline() => document— delegation, focus management() => el— a specific element, whereelis already populated by setup time
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"