- 
                Notifications
    You must be signed in to change notification settings 
- Fork 189
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) => { | ||
|          | ||
| 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 -
elementis goneThis has only one downside -
entrymight be undefined ifinitialInViewistrue: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.
initialInViewwouldn't make sense when used withuseOnViewChangedanyway - 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
useOnInViewChangedmisleading and should beuseOnInViewEnteredoruseOnInViewEffectuseInViewusesuseOnInViewChangedand therefore has to pass over theinitialInViewoption - 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
useOnInViewChangedslightly might get rid of the undefined entry case and handle theinitialInVIewbetterI'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
useInViewsame api like beforeuseOnInViewno longer acceptsinitialInViewuseOnInViewaccepts now atriggeroption (which is set toenterby default but can be changed toleave):that made it way easier to use
useOnInViewinsideuseInViewfor theinitialInViewcaseit also solved the non existing
entrycasewhat 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
entryUh 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: