GithubHelp home page GithubHelp logo

asyarb / use-intersection-observer Goto Github PK

View Code? Open in Web Editor NEW
6.0 6.0 3.0 2.99 MB

Small React hook wrapper around the IntersectionObserver API.

License: MIT License

JavaScript 0.78% HTML 5.66% TypeScript 93.57%

use-intersection-observer's People

Contributors

asyarb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

use-intersection-observer's Issues

jsdom does not support IntersectionObserver

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:

  1. Why do the existing tests work at all?
  2. Would it be possible to use a mock IntersectionObserver instead of the polyfill to test every aspect of the hook thoroughly?

Events Dropped

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?

Callback never updated

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.

Observe not always working when `ref` only is rendered after useIntersectionObserver

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?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.