You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
containerRef.current is read during render, where it is null on the first render (the container hasn't committed yet), and a ref is not reactive — populating .current does not trigger a re-render. So the observer is initially created against the wrong root.
Why this is fragile
Walking through the hook (use-intersection.ts), the returned callback ref is memoized via useCallback keyed on [options?.rootMargin, options?.root, options?.threshold], and it constructs the IntersectionObserver with options when the element attaches:
Render 1:containerRef.current is null → useIntersection({ root: null }). Callback ref memoized with root: null.
Commit: the inner ref attaches → an observer is created with root: null (the viewport), not the intended container.
IntersectionObserver fires once after observe() → setEntry(...) → triggers a re-render.
Render 2: now containerRef.current is the element, so options.root changed → useCallback returns a new ref identity → React detaches the old observer and creates a second observer with the correct container root.
So it only self-corrects because setEntry happens to force a re-render. The cost is a throwaway viewport-rooted observer, an extra render, and a brief entry: null flicker. More importantly, the self-heal only approximates the right behavior while the container is fully within the viewport — if the container is larger than the viewport, partially scrolled off-screen, or if the corrective re-render is suppressed, the result keys off the viewport instead of the intended container.
This is the same root cause as #9025 (useScrollSpyscrollHost): the hook wants a resolved HTMLElement for root, but the idiomatic source is a ref, and the documented pattern papers over the gap by reading .current during render.
Proposed fix
Two parts, either/both:
Docs: update the usage example to drive the container via state (a callback ref into useState), which is deterministic and avoids the throwaway observer:
What package has an issue
@mantine/hooks(v9.3.1) —useIntersectionDescribe the bug
The documented usage example passes
root: containerRef.currentinto the hook:containerRef.currentis read during render, where it isnullon the first render (the container hasn't committed yet), and a ref is not reactive — populating.currentdoes not trigger a re-render. So the observer is initially created against the wrong root.Why this is fragile
Walking through the hook (
use-intersection.ts), the returned callbackrefis memoized viauseCallbackkeyed on[options?.rootMargin, options?.root, options?.threshold], and it constructs theIntersectionObserverwithoptionswhen the element attaches:containerRef.currentisnull→useIntersection({ root: null }). Callback ref memoized withroot: null.refattaches → an observer is created withroot: null(the viewport), not the intended container.IntersectionObserverfires once afterobserve()→setEntry(...)→ triggers a re-render.containerRef.currentis the element, sooptions.rootchanged →useCallbackreturns a new ref identity → React detaches the old observer and creates a second observer with the correct container root.So it only self-corrects because
setEntryhappens to force a re-render. The cost is a throwaway viewport-rooted observer, an extra render, and a briefentry: nullflicker. More importantly, the self-heal only approximates the right behavior while the container is fully within the viewport — if the container is larger than the viewport, partially scrolled off-screen, or if the corrective re-render is suppressed, the result keys off the viewport instead of the intended container.This is the same root cause as #9025 (
useScrollSpyscrollHost): the hook wants a resolvedHTMLElementforroot, but the idiomatic source is a ref, and the documented pattern papers over the gap by reading.currentduring render.Proposed fix
Two parts, either/both:
Docs: update the usage example to drive the container via state (a callback ref into
useState), which is deterministic and avoids the throwaway observer:API (optional, mirrors useScrollSpy: accept a RefObject<HTMLElement> for scrollHost, not just a resolved HTMLElement #9025): allow
rootto accept a ref object and resolve it internally:This is backwards-compatible and lets callers pass the ref they already have without the state dance.
Related
useScrollSpy'sscrollHost.