Search code examples
javascripthtmlcssreactjsinfinite-scroll

How can I stop duplicate array in my React - using infinite scroll?


I have tried to do an infinite scroll by using React-Infinite-Scroll-Component

Everything is working perfectly as I think, but the problem is whenever I change the Genres, or press F5 to reload the page, or somehow, the scrollbar is always at the very bottom, I did try to fix it with window.scrollTo(0,0) but it's still not working.

Or somehow do I mess up the code because I tried to work around it very hard, but I don't think I execute it well enough.

The code is very long but here's a brief explanation of what I'm trying to do in it.

I received a slug which defined as a genre with useParams, for example action or adventure, then I set it as a state genreAnime, then I check if it's the same genre, it'll do the infinite load with concat array and increase the page up to 1 for the next load, else I'll set a new array and initial the page back to 1 or else it'll keep merging with an old array of previous genre. The totalPage is set to 91, whenever it renders for the second time it'll automatically get the latest totalPage and set it back, so it's safe to go.

The translateGenre is just for translating the genre into text, so please don't bother it.

But then whenever I refresh (F5) or whenever I browse for a bit, then I change the genre, it'll get a duplicate of the same first array (due to warning and I see it has 2 same items). Due to the scrollbar of mine always staying at the "end" or at the default value of React-Infinite-Scroll-Component, which is 0.8, it means when users come below 80% of the total height, it'll trigger the next function of Infinite-Scroll-Component

Here's my code:

function AnimeGenre({ instance }) {
    const { genre } = useParams()

    const CancelToken = axios.CancelToken
    const source = CancelToken.source()

    const [animeList, setAnimeList] = useState([])
    const [genreAnime, setGenreAnime] = useState("")
    const [page, setPage] = useState(1)
    const [totalPage, setTotalPage] = useState(91)
    const [translateGenreAnime, setTranslateGenreAnime] = useState("")

    const getList = async () => {
        await instance
            .get(`/anime/${genre}?page=${page}`, {
                cancelToken: source.token,
            })
            .then((response) => {
                const newPage = page + 1
                setPage(newPage)
                const newList = response.data.data.map((anime) => ({
                    slug: anime.slug,
                    thumbnail: anime.thumbnail,
                    name: anime.name,
                    views: anime.views,
                }))
                setTotalPage(response.data.pagination.totalPage)
                setAnimeList((prev) => {
                    return [...new Set([...prev, ...newList])]
                })
            })
            .catch((thrown) => {
                if (axios.isCancel(thrown)) return
            })
    }    
    useEffect(() => {
        if (genre === genreAnime) {
            getList()
            translateGenre()
        } else {
            window.onbeforeunload = function () {
              window.scrollTo(0, 0)
             }
            window.scrollTo({
                top: 0,
            })
            setPage(1)
            setAnimeList([])
            setGenreAnime(genre)
        }

        return () => {
            source.cancel()
        }
    }, [genreAnime, genre])

    const translateGenre = () => {
        for (let i = 0; i < GENRES.length; i++) {
            if (genreAnime == GENRES[i].slug) {
                setTranslateGenreAnime(GENRES[i].name)
            }
        }
    }

    return (
        <>
            <div>
                <h1>ANIME {translateGenreAnime}</h1>
            </div>
            <div className="anime-list">
                <InfiniteScroll
                    initialScrollY={0}
                    style={{ overflow: "none" }}
                    dataLength={animeList.length}
                    next={getList}
                    hasMore={page === totalPage ? false : true}
                    loader={
                        <Row xs={1} sm={2} md={3} lg={4}>
                            {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((skeleton) => (
                                <Col key={skeleton}>
                                    <Card>
                                        <Card.Body style={{ maxHeight: "100%" }}>
                                            <Skeleton
                                                variant="rectangular"
                                                width="100%"
                                                height="192px"
                                                animation="wave"
                                                sx={{ bgcolor: "grey.900" }}
                                            />

                                            <Skeleton
                                                variant="text"
                                                animation="wave"
                                                sx={{ bgcolor: "grey.900" }}
                                            />
                                        </Card.Body>
                                    </Card>
                                </Col>
                            ))}
                        </Row>
                    }
                    endMessage={<h4>NO MORE THINGS TO LOAD</h4>}
                >
                    <Row xs={1} sm={2} md={3} lg={4}>
                        {animeList.map((anime) => (
                            <Col key={anime.slug}>
                                <Card>
                                    <div className="card-container">
                                        <Card.Img
                                            variant="top"
                                            src={anime.thumbnail}
                                            fluid="true"
                                        />
                                        <div className="overlay-card">
                                            <a className="icon">{<BsFillPlayFill size={40} />}</a>
                                        </div>
                                    </div>
                                    <Card.Body>
                                        <Card.Title>
                                            <TextTruncate
                                                line={2}
                                                element="span"
                                                truncateText="…"
                                                text={anime?.name}
                                            />
                                        </Card.Title>
                                    </Card.Body>
                                </Card>
                                <div className="w-100"></div>
                            </Col>
                        ))}
                    </Row>
                </InfiniteScroll>
            </div>
         <>
        }

It's work fine whenever the scrollbar stay at the very top, because it won't surpass the threshold of 80% of the height.

Working one:

Scrollbar on top is work

Fail when I browse a bit and change the GENRE, due to the scrollbar is stay at bottom, then later on, it'll do the scrollTo(0,0). Which is not what I want, I want the opposite, it must always go to top first...

Fail when change GENRE

The second fail is when I press reload the page with F5 button. It'll somehow display very weird (the scrollbar).

F5 fail

UPDATE:

I tried to disable smooth-scrolling behavior and set up the scroll to top into the header component that has the navbar, that's seem more proper way to do it, it's get my scrollbar to the top as I want whenever I clicked something in the header or refresh using F5.

But still if at the first time render, I browse the page and scroll down too fast, I'll get duplicate error due to I meet the threshold and the previous function is still being executed, if I wait for the first render to fully load, then pretty much I am fine, is there any proper way to do infinite scroll, please show me some simple way, I feel like I'm making it too compicated.


Solution

  • I think I got it right, but not so sure, it's working fine, "for now".

    This is what I do.

    Instead of

    const newPage = page + 1
    setPage(newPage)
    

    inside the getList function, I split it out into another function called scrollThreshold

    const scrollThreshold = () => {
        const newPage = page + 1
        setPage(newPage)
    }
    

    and I set

    const PAGE_NUMBER = 1
    

    outside of the component (or else I think I should use useRef or just leave it be, just in case)

    then at the useEffect, I implement 1 more dependencies, it's page state, whenever the I get new page or page state change, or genre change, it'll run the useEffect again.

    then at the InfiniteScroll component, I slightly change it from

    <InfiniteScroll>
    ...
    next={getList}
    ...
    </InfiniteScroll> 
    

    into

    <InfiniteScroll>
    ...
    next={getList && scrollThreshold}
    ...
    </InfiniteScroll> 
    

    So whenever I scroll down, it'll trigger both getList and scrollThreshold, by being separated, it won't clump together like my first code. And so far it's working, I don't know if this is good or not.