Info
I am trying to implement a infinite scroll by also utilizing virtualizing in react. To do this I am using the packages @tanstack/react-query
and @tanstack/react-virtual
.
Problem
I have managed to get it working with the exception that I have to force a re-render to get any content to show up. I have narrowed the problem down to the scrollRef
that useVirtualizer
needs for the getScrollElement
option when initializing.
To get the layout of my application to work correctly I have to move the container that gets the scrollRef to the parent component and set up the useRef
in it to pass down as a prop to the child component so useVirtualizer
can use it and set ref.current
.
If I move the container with the scrollRef and useRef
into the child component where useVirtualizer
is called it works without having to force a re-render, but the layout in my application will not work correctly as it is a electron app that are using a “custom” scrollbar and not the window scrollbar.
Underneath is a simplified version of my code. I have also created a CodeSandbox where you can see the behavior in action.
CodeSandbox link (Click the force re-render button to get the content to show up)
Code
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>
);
}