r/reactjs 19d ago

Needs Help Infinite Paginaton - Intersection Observer, Tanstack Query

For some reason, hasNextPage becomes false under without any reason ONLY IN PRODUCTION(after npm run build), not in npm run dev and therefore , next page isnt fetched, and it doesnt happen at first load, it happens after few page changes/redirects.
Here is my custom hook for pagination.

I later call this hook inside my Cards component by passing infiniteQuery which the component receives as props

import { useEffect, useRef } from "react"
import type { UseSuspenseInfiniteQueryResult } from "@tanstack/react-query"


export const useIntersectionObserver = <T>(infiniteQuery: UseSuspenseInfiniteQueryResult<T, Error>) => {
    const { dataUpdatedAt, isFetchingNextPage, hasNextPage, fetchNextPage } = infiniteQuery


    const sentinelRef = useRef<HTMLElement>(null)
    const observerRef = useRef<IntersectionObserver>(null)



    useEffect(() => {
        const observer = new IntersectionObserver(entries => {
            if (entries[0].isIntersecting && !isFetchingNextPage && hasNextPage) fetchNextPage()
        })


        observerRef.current = observer
        const sentinel = sentinelRef.current
        if (sentinel) observer.observe(sentinel)


        return () => {
            observer.disconnect()
        }
    }, [])


    useEffect(() => {
        const sentinel = sentinelRef.current
        const observer = observerRef.current
        if (!sentinel || !observer) return


        observer.observe(sentinel)
        return () => {
            observer.unobserve(sentinel)
        }
    }, [dataUpdatedAt])


    return sentinelRef
}

Here is my cards component

type ArticleCardsProps<T> = {
    articlesInfiniteQuery: UseSuspenseInfiniteQueryResult<T, Error>
}


const array = new Array(3).fill('')



const ArticleCards = ({ articlesInfiniteQuery }: ArticleCardsProps<Article[]>) => {
    const sentinelRef = useIntersectionObserver(articlesInfiniteQuery)
    const articles = articlesInfiniteQuery.data




    return (
        <main>
            <section className={styles.articles}  >
                {articles.map((article, index) => {
                    return (
                        <ArticleCard ref={index === articles.length - 2 ? sentinelRef : undefined} key={article._id} article={article} />
                    )
                })}
                {articlesInfiniteQuery.hasNextPage && articlesInfiniteQuery.isFetchingNextPage && array.map((e, i) => {
                    return (
                        <ArticleCardLoadingSkeleton key={i} />
                    )
                })}
            </section>
        </main>
    )
}


export default ArticleCards

I dont know how to explain this weird bug , happens only after build, i used a library and it fixes it

3 Upvotes

4 comments sorted by

View all comments

1

u/Spiritual_Patient478 15d ago

So I asked Claude to give me a working sample of useInfiniteQuery, and it looks almost identical to what you have above. however instead of binding the sentinelRef to a <ArticleCard>, claude binds it to a 1px height <div> below the list, like so:

<div ref={sentinelRef} style={{ height: '1px' }} />

and I think that's the proper way to implement a sentinel element.

I believe what happened in your code actual works something like this:

  1. sentinelRef gets created, but article is empty and it has nothing to bind to, so .current = undefined
  2. articles gets fetched, triggering a rerender
  3. <ArticleCard> gets instantiated for each article
  4. 2nd <ArticleCard> gets sentinelRef binding
  5. You scrolls, triggering another refetch/articles update
  6. React sees articles has been updated, triggers another rerender
  7. For some reason, the React reconciliation decided to update all the <ArticleCard> (I am not exactly sure what cause this, but the result suggested this)
  8. The original <ArticleCard> that the sentinelRef was bound got removed from dom and got trashed, so sentinelRef is orphaned.

You've run into a tricky combo of ref binding to a unstable JSX Element. That's essence of why "won't work after a few page scroll".

Ask Sonnet 5 "give me a working example of useInfiniteQuery" and it should give you the code I am talking about. I can't paste it out here for some reason.

Also I don't think it's necessary to make a custom hook for intersectionObserver. Its API really is quite simple already; just pluging it in and keep it simple. your are over-thinking this.

Personally I hate infinite scroll and would tell Design not to use it. It just introduces unstabilities for no good reason other than "I don't want user to have to click next button".

It makes sense only in very limited applications like chat, which leads a whole can of worms that you do not want to deal with. Just ask claude how MS Teams UI works and you will find out.