Skip to content

v6.5.4

Choose a tag to compare

@childrentime childrentime released this 20 Aug 09:30
· 14 commits to main since this release

🐞 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() restored overflow: hidden — the page never scrolled again
  • an existing inline value such as overflow: overlay was 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