Skip to content

fix(useElementByPoint): avoid a re-render on every frame in multiple mode - #214

Merged
childrentime merged 1 commit into
childrentime:mainfrom
ostapondo:fix/element-by-point-frame-rerender
Jul 30, 2026
Merged

fix(useElementByPoint): avoid a re-render on every frame in multiple mode#214
childrentime merged 1 commit into
childrentime:mainfrom
ostapondo:fix/element-by-point-frame-rerender

Conversation

@ostapondo

Copy link
Copy Markdown
Contributor

Description

With multiple: true, useElementByPoint re-renders the consuming component on
every animation frame, even when the pointer hasn't moved and the elements under
it haven't changed.

elementsFromPoint() returns a freshly allocated array on each call, so the rAF
loop hands setElement a new reference every frame. React compares by identity,
sees a change, and re-renders — roughly 60 times a second, for as long as the
hook is active. The single-element path doesn't have this problem, since
elementFromPoint() returns the same node and React bails out on its own.

The fix compares the hit list by contents and keeps the previous array when
nothing changed. I used the functional form of setElement so the comparison
sees the committed value without needing an extra ref.

Type of Change

  • Bug fix
  • New hook
  • Enhancement to existing hook
  • Documentation update
  • Other (please describe)

Checklist

  • I have read the Contributing Guide
  • My code follows the project's coding style
  • I have added tests for my changes
  • All existing tests pass
  • I have updated the documentation

No doc change — public API and return values are unchanged.

This hook had no spec file, so I added one. Three tests: the element is reported
for the single case, an unchanged hit list no longer re-renders (3 renders over 3
frames on main, 0 here), and a hit list that genuinely changes still updates —
that last one matters, since a comparison like this is exactly the kind of change
that can silently freeze state.

…mode

elementsFromPoint() allocates a new array on every call, so the rAF loop
handed setElement a fresh reference each frame and re-rendered the
consuming component roughly 60 times a second even while the pointer sat
still. Compare the hit list by contents and keep the previous array when
nothing under the point changed. The single-element branch needs no such
check - elementFromPoint returns the same node and React bails out on
its own.

Adds the first tests for this hook, covering both that an unchanged hit
list does not re-render and that a changed one still does.
@childrentime
childrentime merged commit 5a73065 into childrentime:main Jul 30, 2026
5 checks passed
@childrentime

Copy link
Copy Markdown
Owner

Thanks @ostapondo! Killing a 60fps re-render loop while keeping the array identity stable is a real win for anyone memoizing on element, and this hook finally has a spec file. Especially appreciated the third test — guarding the optimization against silently freezing state is exactly the right instinct. Four excellent PRs in a row (#212#215): real bugs, minimal diffs, tests that fail on main first. Contributions like these are a maintainer's dream — hope to see you around! 🎉

childrentime added a commit that referenced this pull request Jul 30, 2026
…eck gate

#214 was written before the typecheck gate from #211 landed: `next` is
`Element[] | Element | null`, and the `multiple &&` guard can't narrow it,
so `next.length` / `next[i]` fail tsc. Guard with `Array.isArray(next)`
instead — runtime-equivalent, since the single-element branch never
produces an array — and type the spec's reassigned hit list as `Element[]`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
childrentime added a commit that referenced this pull request Jul 31, 2026
Fixes
- useOrientation: lockOrientation/unlockOrientation had inverted isBrowser
  guards — they early-returned in the browser and only ran during SSR,
  making both no-ops where they mattered. Closes #215.
- useInterval: with controls: true, an interval started through resume()
  kept firing after unmount, and calling resume() twice leaked a timer
  neither pause() nor unmount could reach. Closes #212.
- useMicrophone: level stayed frozen at the last reading after stop();
  it now resets to 0. Closes #213.
- useElementByPoint: multiple mode re-rendered on every rAF frame because
  elementsFromPoint allocates a fresh array; the hit list is now compared
  element-by-element and kept stable when unchanged. Closes #214.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants