使用 GraphQL 和 IntersectionObserver 的无限滚动组件

Infinite scroll component using GraphQL and IntersectionObserver

我对 React 的经验很少,我正在尝试使用 GraphQL 和 IntersectionObserver 进行无限滚动,一切正常,但我得到的结果是我要求的两倍。我猜这是来自双重渲染,一个来自@apollo/client fetchMore,另一个来自 IntersectionObserver 挂钩,但我不太明白它,我不知道如何修复它。

IntersectionObserver 挂钩:

import { useCallback, useRef, useState } from "react";

export default function useIntersection(options?: IntersectionObserverInit): IntersectionHook {
    const [entry, setEntry] = useState<IntersectionObserverEntry>();
    const observer = useRef<IntersectionObserver>();
    const target = useRef<Element | null>(null);

    const setSentinel = useCallback<IntersectionHook[0]>(
        (sentinel) => {
            if (!observer.current) {
                observer.current = new IntersectionObserver((entries) => setEntry(entries[0]), options);
            }

            if (target.current) {
                observer.current.unobserve(target.current);
            }

            target.current = sentinel;

            if (target.current) {
                observer.current.observe(target.current);
            }
        },
        [options],
    );

    return [setSentinel, entry];
}

type IntersectionHook = [(sentinel: Element | null) => void, IntersectionObserverEntry | undefined];

使用钩子的组件:

import { Fragment, ReactElement, ReactNode, useEffect } from "react";
import useIntersection from "../hooks/intersectionHook";

export default function InfiniteComponent(props: InfiniteComponentProps): ReactElement {
    const [setSentinel, entry] = useIntersection();

    useEffect(() => {
        if (entry?.isIntersecting) {
            props.onNext();
        }
    }, [props, entry]);

    return (
        <Fragment>
            {props.children}
            {props.next && <div ref={setSentinel}></div>}
        </Fragment>
    );
}

export interface InfiniteComponentProps {
    next?: boolean;
    onNext: () => Promise<unknown>;
    children: ReactNode;
}

使用该组件的页面:

export default function IndexArtist(): ReactElement | null {
    const { loading, error, data, fetchMore } = useQuery<IndexArtistData, IndexArtistVars>(INDEX_ARTIST, {
        variables: { limit: 3 },
    });

    if (loading) {
        return null;
    }

    if (error) {
        return <Redirect to="/error" />;
    }

    if (!data?.artists.nodes.length) {
        return <Redirect to="/error" />;
    }

    return (
        <BoxElement $variant="page">
            <InfiniteComponent
                next={data.artists.page.next}
                onNext={async () => fetchMore({ variables: { cursor: data.artists.page.cursor } })}
            >
                {/* The code to render */}
            </InfiniteComponent>
        </BoxElement>

使用上面的代码,我请求了 3 个结果,并且在加载页面时得到了 3 个结果,但是当我向下滚动直到 IntersectionObserver 挂钩的 sentinel 以获取更多结果时,我得到了 6 个结果(首先我得到 3 个结果,但不久之后,几毫秒后,我得到了其他 3 个结果。

我仍然乐于接受建议。下一个解决方案似乎不是更优雅,但这是我找到的唯一解决方案。

使用钩子的组件:

import { Fragment, ReactElement, ReactNode, useEffect, useRef, useState } from "react";
import useIntersection from "../hooks/intersectionHook";

export default function InfiniteComponent(props: InfiniteComponentProps): ReactElement {
    const [wait, setWait] = useState(false);
    const ready = useRef(!wait);
    const [entry, setSentinel] = useIntersection();

    useEffect(() => {
        ready.current = !wait;
    }, [wait]);

    useEffect(() => {
        const fetchMore = async (): Promise<void> => {
            setWait(true);
            await props.onMore();
            setWait(false);
        };

        if (ready.current && entry?.isIntersecting) {
            fetchMore();
        }
    }, [entry, props]);

    return (
        <Fragment>
            {props.children}
            {props.next && !wait && <div ref={setSentinel}></div>}
        </Fragment>
    );
}

export interface InfiniteComponentProps {
    next?: boolean;
    onMore: () => Promise<unknown>;
    children: ReactNode;
}