Skip to content

[@mantine/hooks] use-intersection: Accept a ref object for the root option - #9054

Open
h-harsh wants to merge 1 commit into
mantinedev:masterfrom
h-harsh:feat/use-intersection-ref-root
Open

[@mantine/hooks] use-intersection: Accept a ref object for the root option#9054
h-harsh wants to merge 1 commit into
mantinedev:masterfrom
h-harsh:feat/use-intersection-ref-root

Conversation

@h-harsh

@h-harsh h-harsh commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Closes #9026

useIntersection's root option previously only accepted a resolved Element, so the idiomatic React source — a ref — couldn't be used directly. ref.current is null on the first render and reading it during render is fragile, which is exactly what the documented usage example did (root: containerRef.current).

This mirrors #9031, which added ref support to useScrollSpy's scrollHost.

The catch (why this isn't a one-liner)

useScrollSpy attaches its listener inside a useEffect, so ref.current is already populated when it runs. useIntersection is different: it built the IntersectionObserver synchronously inside the target's callback ref. Because React attaches child refs before their parent's, a root ref is still null at that moment — so naively resolving root.current there would reproduce the exact bug, just relocated.

The fix moves observer creation into a useEffect (runs after all refs are committed), tracking the observed element in state via the callback ref. As a bonus this removes the old throwaway viewport-rooted observer + extra render that the previous root: ref.current pattern caused.

Changes

  • Widen the option type via a new, exported UseIntersectionOptions (root?: IntersectionObserverInit['root'] | React.RefObject<Element | null>).
  • Resolve a ref object to its element before constructing the observer; passing a resolved Element is unchanged.
  • Add the hook's first test file — root resolution (element / ref / null-current / viewport default), option forwarding, entry updates, cleanup on detach/unmount, and an end-to-end render proving the ref root resolves correctly despite child-before-parent ref attachment order.
  • Update the docs and usage demo to pass the ref directly (root: containerRef).

Notes

Verification

  • use-intersection tests: 11 passing
  • tsc --noEmit clean, oxlint clean, formatted
  • @mantine/hooks builds (incl. .d.ts generation)

…ption

Closes mantinedev#9026

`useIntersection`'s `root` option previously only accepted a resolved
`Element`, so the idiomatic React source (a ref) could not be used
directly: `ref.current` is `null` on the first render and reading it
during render was fragile. The documented usage example relied on that
fragile `root: containerRef.current` pattern.

`root` now also accepts a `RefObject<Element | null>`. The observer is
created inside a `useEffect` (after refs are committed), so the ref's
`current` is resolved once the element is mounted — no throwaway
viewport-rooted observer and no `.current`-during-render read. Passing a
resolved `Element` continues to work unchanged.

- Widen the option type via new `UseIntersectionOptions` (exported).
- Resolve a ref object to its element before constructing the observer.
- Add the hook's first test file (root resolution, viewport fallback,
  entry updates, cleanup, and an end-to-end render proving the ref root
  resolves despite child-before-parent ref attachment order).
- Update docs and the usage demo to pass the ref directly.
@h-harsh
h-harsh force-pushed the feat/use-intersection-ref-root branch from a56105d to 77d260a Compare July 12, 2026 19:23
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.

useIntersection: documented root: ref.current usage is a fragile init-order pattern

1 participant