v6.5.4
🐞 Bug Fixes
useEventListener: passes the listener options back to removeEventListener
Registration called addEventListener(name, fn, options), but cleanup called removeEventListener(name, fn) with the options dropped. The DOM matches a listener for removal on (type, callback, capture), so anything registered with { capture: true } — or the boolean true form — was never detached:
function Modal() {
// intercept clicks before they reach the page
useEventListener("click", onCapture, () => document, { capture: true });
return <div role="dialog">…</div>;
}Close the modal and the handler keeps firing. The listener also survived a change of eventName or options, and accumulated one live listener per mount — five mount/unmount cycles left five orphaned handlers, all firing on a single click.
passive, once and { capture: false } were unaffected, since only capture participates in the removal match. Verified in jsdom and Chrome 151.
useScrollLock: snapshots the original overflow once per lock
The effect that applies the lock lists target in its dependencies, and the () => document.body getter the docs recommend for SSR safety is a fresh function on every render — so the effect re-ran on every render and re-executed:
initialOverflowRef.current = element.style.overflow;While locked, that reads back the hidden the hook itself just wrote. One unrelated re-render with the modal open was enough to poison the saved value, and then:
unlock()restoredoverflow: hidden— the page never scrolled again- an existing inline value such as
overflow: overlaywas swallowed - the v6.5.3 unmount cleanup restored the poisoned value too
Only the path with no render between lock and unlock behaved correctly, which is why hand testing never caught it.
The snapshot is now keyed on the locked element rather than on the effect firing, so it happens once per lock however often the effect re-runs — initialState: true included. If the target moves while locked, the element being released is restored before the new one is captured.
Thanks to @hayrullahkar for the report and the diagnosis.
✅ Tests
Twelve regression tests, all of which fail on 6.5.3: capture-listener detachment on unmount, the boolean capture form, the arguments handed to removeEventListener, dependency changes, accumulation across five mount cycles, and the non-capture control path; plus re-render while locked, a non-default inline value, unmount after a re-render, initialState with a re-render, repeated lock cycles, and a target swap mid-lock.
Full changelog: v6.5.3...v6.5.4