-
Notifications
You must be signed in to change notification settings - Fork 190
use of React 19 ref callbacks for IntersectionObserver tracking #718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
148d326
d4bb4c4
2ff6b92
6620732
27a0793
58c46bc
ff7c78e
7c25f82
d9523b6
2b5e7f0
33d4bab
8b5e64e
d67bde9
4399d1b
6cff96f
194997e
ff3e88c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,6 +236,60 @@ You can read more about this on these links: | |
- [w3c/IntersectionObserver: Cannot track intersection with an iframe's viewport](https://github.com/w3c/IntersectionObserver/issues/372) | ||
- [w3c/Support iframe viewport tracking](https://github.com/w3c/IntersectionObserver/pull/465) | ||
|
||
### `useOnInViewChanged` hook | ||
|
||
```js | ||
const inViewRef = useOnInViewChanged( | ||
(element, entry) => { | ||
// Do something with the element that came into view | ||
console.log('Element is in view', element); | ||
|
||
// Optionally return a cleanup function | ||
return (entry) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would be a usecase for the cleanup function for the consumer? You shouldn't use it to track if elements are leaving, where it's better to observe the callback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The advantage of a cleanup is that you have access to the closure of the viewport entering Some example cases:
It's also the pattern for |
||
console.log('Element moved out of view or unmounted'); | ||
}; | ||
}, | ||
options // Optional IntersectionObserver options | ||
); | ||
``` | ||
|
||
The `useOnInViewChanged` hook provides a more direct alternative to `useInView`. It takes a callback function and returns a ref that you can assign to the DOM element you want to monitor. When the element enters the viewport, your callback will be triggered. | ||
|
||
Key differences from `useInView`: | ||
- **No re-renders** - This hook doesn't update any state, making it ideal for performance-critical scenarios | ||
- **Direct element access** - Your callback receives the actual DOM element and the IntersectionObserverEntry | ||
- **Optional cleanup** - Return a function from your callback to run when the element leaves the viewport or is unmounted | ||
- **Same options** - Accepts all the same [options](#options) as `useInView` except `onChange` | ||
|
||
```jsx | ||
import React from "react"; | ||
import { useOnInViewChanged } from "react-intersection-observer"; | ||
|
||
const Component = () => { | ||
// Track when element appears without causing re-renders | ||
const trackingRef = useOnInViewChanged((element, entry) => { | ||
// Element is in view - perhaps log an impression | ||
console.log("Element appeared in view"); | ||
|
||
// Return optional cleanup function | ||
return () => { | ||
console.log("Element left view"); | ||
}; | ||
}, { | ||
/* Optional options */ | ||
threshold: 0.5, | ||
}); | ||
|
||
return ( | ||
<div ref={trackingRef}> | ||
<h2>This element is being tracked without re-renders</h2> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Component; | ||
``` | ||
|
||
## Testing | ||
|
||
> [!TIP] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,17 +107,17 @@ | |
} | ||
], | ||
"peerDependencies": { | ||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0", | ||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" | ||
"react": "^19.0.0", | ||
"react-dom": "^19.0.0" | ||
Comment on lines
+111
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @thebuilder @jantimon, thanks for being careful about supporting this without breaking React 18, it's critical for several projects we're working on. Some projects can't upgrade to React 19 anytime soon due to legacy dependencies that may never support it. Since React 19 is still recent, many packages lack support, so upgrading isn't an option yet. If React 19 becomes the only target, a major version bump would likely be needed to avoid breaking existing setups. Appreciate the careful consideration! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the input. And I agree - We shouldn't just break React 18. I supported React 15 and 16 until last year. |
||
}, | ||
"devDependencies": { | ||
"@arethetypeswrong/cli": "^0.17.2", | ||
"@biomejs/biome": "^1.9.4", | ||
"@size-limit/preset-small-lib": "^11.1.6", | ||
"@testing-library/jest-dom": "^6.6.3", | ||
"@testing-library/react": "^16.1.0", | ||
"@types/react": "^19.0.2", | ||
"@types/react-dom": "^19.0.2", | ||
"@types/react": "^19.0.10", | ||
"@types/react-dom": "^19.0.4", | ||
"@vitejs/plugin-react": "^4.3.4", | ||
"@vitest/browser": "^2.1.8", | ||
"@vitest/coverage-istanbul": "^2.1.8", | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the consumer needs the
element
, they should be able to get it from entry.target.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the idea -
element
is goneThis has only one downside -
entry
might be undefined ifinitialInView
istrue
:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialInView
wouldn't make sense when used withuseOnViewChanged
anyway - It's for avoiding flashing content on the initial render.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe the name
useOnInViewChanged
misleading and should beuseOnInViewEntered
oruseOnInViewEffect
useInView
usesuseOnInViewChanged
and therefore has to pass over theinitialInView
option - otherwise it is not possible to update the state on out of viewThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe changing the api of
useOnInViewChanged
slightly might get rid of the undefined entry case and handle theinitialInVIew
betterI'll let you know if it works
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, and I agree with the name. I have been considering building that hook before, but got stuck on the finding the right name. I might be more into
useOnInView
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I refactored the code
useInView
same api like beforeuseOnInView
no longer acceptsinitialInView
useOnInView
accepts now atrigger
option (which is set toenter
by default but can be changed toleave
):that made it way easier to use
useOnInView
insideuseInView
for theinitialInView
caseit also solved the non existing
entry
casewhat do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good - Did you try it with multiple thresholds? Would it just trigger multiple times? Should be fine, as long as it can then be read from the
entry
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh good catch - I found a missing cleanup edge case - now it's fixed and tested: