信息
我正在尝试通过在反应中利用虚拟化来实现无限滚动。为此,我使用包
@tanstack/react-query
和 @tanstack/react-virtual
。
问题
我已经设法让它工作,但我必须强制重新渲染才能显示任何内容。我已将问题范围缩小到初始化时
scrollRef
选项所需的 useVirtualizer
。为了让我的应用程序的布局正常工作,我必须将获取scrollRef的容器移动到父组件,并在其中设置getScrollElement
以作为道具传递给子组件,以便
useRef
可以使用并设置 useVirtualizer
。如果我将带有scrollRef和ref.current
的容器移动到调用
useRef
的子组件中,它可以工作而无需强制重新渲染,但我的应用程序中的布局将无法正常工作,因为它是一个电子应用程序使用“自定义”滚动条而不是窗口滚动条。下面是我的代码的简化版本。我还创建了一个 CodeSandbox,您可以在其中查看实际行为。
CodeSandbox 链接代码
useVirtualizer
如果您使用 useState 而不是使用 useRef 将 ref 存储在状态中,则它可以正常工作。
const Shows = ({
scrollRef,
}: {
scrollRef: React.RefObject<HTMLDivElement>;
}) => {
const [, setForceRerender] = useState(0);
const { data, isFetchingNextPage, fetchNextPage, hasNextPage } =
useSuspenseInfiniteQuery({
queryKey: ["shows"],
queryFn: async ({ pageParam = 1 }) => {
const { data } = await axios.get(
"https://demoapi.com?page=" + pageParam,
);
return data;
},
initialPageParam: 1,
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.total_pages ? lastPage.page + 1 : undefined,
});
const shows = data ? data.pages.flatMap((page) => page.results) : [];
const columnCount = 5;
const rowCount = Math.ceil(shows.length / columnCount);
const itemWidth = 166;
const itemHeight = 248;
const rowVirtualizer = useVirtualizer({
count: hasNextPage ? rowCount + 1 : rowCount,
getScrollElement: () => scrollRef.current,
estimateSize: () => itemHeight,
overscan: 1,
});
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: columnCount,
getScrollElement: () => scrollRef.current,
estimateSize: () => itemWidth,
overscan: 0,
});
const rowItems = rowVirtualizer.getVirtualItems();
useEffect(() => {
const [lastItem] = [...rowItems].reverse();
if (!lastItem) {
return;
}
if (
(lastItem.index + 1) * columnCount >= shows.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
}, [hasNextPage, fetchNextPage, shows.length, isFetchingNextPage, rowItems]);
return (
<div style={{ width: "100%" }}>
<button onClick={() => setForceRerender((prev) => prev + 1)}>
Force rerender
</button>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
position: "relative",
width: "100%",
userSelect: "none",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) =>
columnVirtualizer.getVirtualItems().map((virtualColumn) => {
const isLoaderRow =
virtualRow.index > Math.ceil(shows.length / 5) - 1;
const index = virtualColumn.index + virtualRow.index * columnCount;
const show = shows[index];
return (
<div
key={index}
style={{
position: "absolute",
top: 0,
left: 0,
width: `${virtualColumn.size}px`,
height: `${virtualRow.size}px`,
transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
}}
>
{!isLoaderRow && show && (
<div key={index} style={{ cursor: "pointer" }}>
<img
style={{
height: "232px",
width: "150px",
objectFit: "fill",
}}
src={`${
show.poster_path
? `https://image.tmdb.org/t/p/w185/${show.poster_path}`
: "https://via.placeholder.com/185x278?text=Image+missing"
}`}
alt=""
/>
</div>
)}
</div>
);
}),
)}
</div>
{shows.length === 0 && (
<div className="flex h-full w-full items-center justify-center">
No shows to discover that matches your filters
</div>
)}
{!hasNextPage && shows.length > 0 && (
<div className="mt-2 flex justify-center">
No more shows to discover
</div>
)}
{isFetchingNextPage && (
<div className="flex items-center">Loading next page...</div>
)}
</div>
);
};
function App() {
const scrollRef = useRef<HTMLDivElement>(null);
return (
<Suspense fallback={<div>Loading in app.tsx...</div>}>
<div
style={{
height: "500px",
overflow: "auto",
}}
ref={scrollRef}
>
<Shows scrollRef={scrollRef} />
</div>
</Suspense>
);
}
然后将 ref 的用途从
const [scrollRef, setScrollRef] = useState<HTMLDivElement | null>(null);
<div ref={scrollRef}>...</div>
更改为
scrollRef.current