asyarb / use-intersection-observer Goto Github PK
View Code? Open in Web Editor NEWSmall React hook wrapper around the IntersectionObserver API.
License: MIT License
Small React hook wrapper around the IntersectionObserver API.
License: MIT License
I tried to write tests for the callback
, but it behaved weirdly in tests while working normally when running the application. Then I found out that jsdom does not do any layouting and therefore cannot support IntersectionObserver: jsdom/jsdom#2032 (comment)
Note that jsdom still does not do any layout or rendering, so this is really just about pretending to be visual, not about implementing the parts of the platform a real, visual web browser would implement.
So I ask myself:
This hook has a major issue in this line:
const [entry] = entries
The browser might defer calling the intersection observer callback when it is busy doing other stuff. In that case, the callback will be called with multiple elements in the entries
array. That's why it's an array in the first place.
Here's a related webkit bug report that pinned down a production problem to this browser behavior:
https://bugs.chromium.org/p/chromium/issues/detail?id=941681#c7
In order to fix this, you need to do 2 different behaviors, depending on triggerOnce
:
const handleIntersect = (entries: IntersectionObserverEntry[]) => {
if (!intersectObs) return
if (options.triggerOnce) {
const firstIntersectingEntry = entries.find(e => e.isIntersecting)
if (typeof firstIntersectingEntry === 'undefined') {
setInView(false)
} else {
callback?.(firstIntersectingEntry)
observer.disconnect()
setInView(true)
}
} else {
// INFO: there might be multiple elements in the entries array when the browser was busy doing other things - in that case, we drop all entries but the last one to avoid unnecessary rerenders (will probably be obsolete when concurrent mode arrives) - client code should not depend on the callback being called for every single entry in this case, rather consider using triggerOnce
const lastEntry = entries[entries.length - 1]
callback?.(lastEntry)
setInView(lastEntry.isIntersecting)
}
}
The behavior with triggerOnce=false
described by the INFO
comment is debatable, maybe it makes more sense to execute the callback with all elements of the entries
array?
While researching another problem, I noticed that useIntersectionObserver
is never updating the callback
:
const [intersectObs] = useState(() =>
IS_BROWSER ? new IntersectionObserver(handleIntersect, options) : undefined
)
Since the initial state of useState
is only executed for the first run of the hook, that means intersectObs
will always point to an instance of IntersectionObserver
with the handleIntersect
function of the first run of the hook.
But handleIntersect
changes for every run of the hook, and even when useCallback
would be used for it, that still would change depending on its dependency array.
I do not really understand why this seems to make no problems? Theoretically, when either callback
or triggerOnce
is changing between runs of useIntersectionObserver
, it would not pick up these changes, and still execute the first intersectObs
callback. Maybe I need to test this differently.
If this is really a problem, a solution would be to use something like this:
const handleIntersect = useCallback(
(entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {...},
[callback, options.triggerOnce]
)
// because of useState, the callback function is never updated, therefore we need to use a stable callback that never changes by using an empty dependency array
const handleIntersectRef = useRef(handleIntersect)
handleIntersectRef.current = handleIntersect
const handleIntersectStable = useCallback((entries: IntersectionObserverEntry[], observer: IntersectionObserver) => handleIntersectRef.current(entries, observer), [])
const [intersectObs] = useState(() => (IS_BROWSER ? new IntersectionObserver(handleIntersectStable, options) : null))
But this also doesn‘t update the options
of the IntersectionObserver. Or maybe use useMemo
for the IntersectionObserver?
const intersectObs = useMemo(() => (IS_BROWSER ? new IntersectionObserver(handleIntersect, options) : null), [handleIntersect, options])
The two ideas could also be combined to both avoid unnecessary resubscribes when only the callback changes, and allow to change the options.
I have a problem when I render a div with the ref
to be observed conditionally:
export const MyComponent = () => {
const [isLoading, setIsLoading] = useState<boolean>(true)
useEffect(() => {
async function fetchSimilarAds() {
setTimeout(() => {
setIsLoading(false)
}, 100)
}
// avoid showing old similar ads when client side paging through ads
setSearchResult(undefined)
fetchSimilarAds()
}, [])
const visibilityCallback = useCallback(() => {
console.log('visibilityCallback triggered') // BUG: never is triggered
}, [])
const visibilityTrackingRef = useRef<HTMLDivElement>(null)
useIntersectionObserver(visibilityTrackingRef, null, { triggerOnce: false, threshold: 0.7 }, visibilityCallback)
if (isLoading) {
return <Skeleton />
}
return (
<div ref={visibilityTrackingRef}>...content...</div>
}
I found that the problem is that React only sets the visibilityTrackingRef
after the component has been rendered (makes sense), and such also after useIntersectionObserver
runs, while useEffect
inside useIntersectionObserver
would run after that, but because of the dependency array on useEffect
([ref, intersectObs, element]
), it is skipped for the last render.
I don't really have an idea how to solve this without an additional useState
in useIntersectionObserver
which comes with the downside that it causes a rerender of MyComponent
. Any suggestions?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.