Skip to content

v6.5.7

Choose a tag to compare

@childrentime childrentime released this 15 Sep 02:22
· 19 commits to main since this release

Four bug fixes, all found and fixed by @rawsun007. Most of it is cleanup that never ran: listeners, timers and object URLs that outlived the component that created them.

🐞 Bug Fixes

useObjectUrl: revokes the URLs it creates

The cleanup read url from the render closure — the render before setUrl applied — so it was always undefined there and URL.revokeObjectURL was never called. Every object URL the hook created stayed alive until the page closed, although the docs said the opposite.

const [file, setFile] = useState<File>()
const url = useObjectUrl(file)

// pick three files in a row, then unmount
// 6.5.6: three live object URLs, none ever revoked
// Now:   each change revokes the previous URL, unmount revokes the last one

The hook also returns undefined again once the object goes away, instead of the URL of an object it no longer has. And the argument is now typed as optional (object?: Blob | MediaSource), so the snippet above — the documented usage — typechecks as written. (#226)

useKeyModifier: detaches its listeners and follows a changed modifier

The four document listeners were registered through useMount, which ignores a returned cleanup, so every mount left four listeners behind, each calling setState on an unmounted component whenever a key or button was pressed. The empty dependency list froze the arguments too:

const [modifier, setModifier] = useState<KeyModifier>('Shift')
const pressed = useKeyModifier(modifier)

setModifier('Control')
// 6.5.6: `pressed` still tracks Shift.  Now: it tracks Control.

The same held for options.events. Registration now happens in an effect keyed on both, with one handler reference shared by add and remove — the shape useEventListener already uses. (#223)

useDoubleClick: keeps the click count across renders and cancels its timer

The click count was a local rebuilt on every render, so any re-render between two clicks — a parent update, a ticking clock — reset it:

click()
rerender()
click()
// 6.5.6: onSingleClick called twice.  Now: onDoubleClick called once.

The pending setTimeout was never stored either, so it could not be cancelled and ran its callback after unmount. Count and timer now live in a ref (one pair per event type, so click and touchend stay independent), the timer is cleared on teardown, a callback that throws no longer wedges the hook, and the timer always calls the callbacks from the latest render. (#224)

useMousePressed: attaches the touch and drag listeners to a ref target

const ref = useRef<HTMLDivElement>(null)
const [pressed, sourceType] = useMousePressed(ref)
return <div ref={ref} />

The target was resolved during render, where ref.current is still null, and nothing re-ran to pick it up once the element mounted. So with this — the usual pattern — touchstart and dragstart were never attached, and touch or drag presses on the element went unreported; only mousedown worked, because it goes through useEventListener. The target now resolves inside the effect. dragstart and touchstart were also never detached, since the add and the remove each received a freshly built handler; there is one stable handler per event now. (#225)

✅ Tests

420 tests, up from 391. useKeyModifier, useMousePressed and useObjectUrl gain their first spec files (7, 6 and 5 tests), and useDoubleClick's placeholder grows to 12. Thirteen of the new tests fail on 6.5.6.

Full changelog: v6.5.6...v6.5.7