使用 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;
}
我对 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;
}