react-virtualized
react-virtualized copied to clipboard
InfiniteLoader jumps when scrolling up after loadMoreRows completes
I have a react-virtualised
InfiniteLoader
consisting of single rows.
The main issue I believe, is that each cell can vary in height and have to load in different images for each so the height is not static and changes as the images load in. But I am still seeing the issue even when the all the cells are the exact same height.
This is my current component using react-virtualised
InfiniteLoader
with Grid
/* eslint-disable no-underscore-dangle */
import React, {
FC,
LegacyRef,
useEffect,
useLayoutEffect,
useMemo,
useRef
} from "react";
import {
InfiniteLoader,
Grid,
SectionRenderedParams,
AutoSizer,
WindowScroller,
GridCellProps,
ColumnSizer,
CellMeasurerCache,
CellMeasurer,
Index,
InfiniteLoaderChildProps,
WindowScrollerChildProps,
Size,
SizedColumnProps
} from "react-virtualized";
import { CellMeasurerChildProps } from "react-virtualized/dist/es/CellMeasurer";
import PuffLoader from "react-spinners/PuffLoader";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import styled from "styled-components";
const LOADER_SIZE = 100;
const LoaderWrapper = styled.div`
width: calc(100% - ${LOADER_SIZE}px);
text-align: center;
height: ${LOADER_SIZE}px;
margin: 15px 0px;
`;
interface InfiniteGridProps {
items: any[] | undefined;
defaultHeight?: number | undefined;
loadMoreItems?: () => Promise<void>;
totalResults?: number | undefined;
overscanRowCount?: number;
renderItem: (props: any, rowIndex: number) => React.ReactNode | undefined;
preventScrollLoader?: boolean;
}
interface GridParent {
_scrollingContainer?: any;
}
interface IGridCellProps extends GridCellProps {
parent: GridCellProps["parent"] & GridParent;
}
interface InfiniteGridItemProps {
renderItem: InfiniteGridProps["renderItem"];
gridItem: any;
reCalculateGrid: (
rowIndex: IGridCellProps["rowIndex"],
columnIndex: IGridCellProps["columnIndex"],
measure: CellMeasurerChildProps["measure"]
) => void;
rowIndex: IGridCellProps["rowIndex"];
columnIndex: IGridCellProps["columnIndex"];
parent: IGridCellProps["parent"];
measure: CellMeasurerChildProps["measure"];
}
const InfiniteGridItem: React.FC<InfiniteGridItemProps> = ({
renderItem,
gridItem,
reCalculateGrid,
rowIndex,
columnIndex,
parent,
measure
}) => {
const [rowRef, { height }] = useMeasure({ polyfill: ResizeObserver });
useLayoutEffect(() => {
reCalculateGrid(
rowIndex,
columnIndex,
parent._scrollingContainer ? measure : () => {}
);
}, [
height,
columnIndex,
measure,
parent._scrollingContainer,
reCalculateGrid,
rowIndex
]);
return <div ref={rowRef}>{renderItem(gridItem, rowIndex)}</div>;
};
const InfiniteGrid: FC<InfiniteGridProps> = ({
items,
defaultHeight = 300,
loadMoreItems,
totalResults,
overscanRowCount = 10,
renderItem
}) => {
const loaderRef = useRef<InfiniteLoader | undefined>();
const cache = useMemo(
() =>
new CellMeasurerCache({
fixedWidth: true,
defaultHeight
}),
[defaultHeight]
);
const onResize = () => {
cache.clearAll();
if (loaderRef && loaderRef.current) {
loaderRef.current.resetLoadMoreRowsCache(true);
}
};
const reCalculateGrid = (
rowIndex: IGridCellProps["rowIndex"],
columnIndex: IGridCellProps["columnIndex"],
measure: CellMeasurerChildProps["measure"]
) => {
cache.clear(rowIndex, columnIndex);
measure();
};
const isRowLoaded = ({ index }: Index) => {
if (items && totalResults !== undefined) {
const isLoaded = !!items[index] || totalResults <= items.length;
return isLoaded;
}
return false;
};
const loadMoreRows = async () => {
if (loadMoreItems) await loadMoreItems();
};
const cellRenderer = (
{ rowIndex, columnIndex, style, key, parent }: IGridCellProps,
columnCount: number
) => {
const index = rowIndex * columnCount + columnIndex;
const gridItem = items?.[index];
if (!gridItem || !renderItem) return null;
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={columnIndex}
rowIndex={rowIndex}
>
{({ registerChild, measure }: any) => (
<div
ref={registerChild}
style={{
...style,
overflow: "visible"
}}
key={key}
>
<InfiniteGridItem
renderItem={renderItem}
gridItem={gridItem}
reCalculateGrid={reCalculateGrid}
rowIndex={rowIndex}
columnIndex={columnIndex}
parent={parent}
measure={measure}
/>
</div>
)}
</CellMeasurer>
);
};
useEffect(() => {
cache.clearAll();
if (loaderRef && loaderRef.current) {
loaderRef.current.resetLoadMoreRowsCache(true);
}
}, [loaderRef, cache, items]);
const infiniteLoaderRender = () => (
<WindowScroller>
{({
height,
onChildScroll,
scrollTop,
registerChild
}: WindowScrollerChildProps) => (
<div ref={registerChild}>
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={totalResults}
threshold={1}
ref={loaderRef as LegacyRef<InfiniteLoader> | undefined}
>
{({ onRowsRendered }: InfiniteLoaderChildProps) => (
<AutoSizer disableHeight onResize={onResize}>
{({ width }: Size) => {
const columnCount = Math.max(Math.floor(width / width), 1);
return (
<ColumnSizer width={width} columnCount={columnCount}>
{({ registerChild: rg }: SizedColumnProps) =>
loaderRef && loaderRef.current ? (
<Grid
autoHeight
width={width}
height={height}
scrollTop={scrollTop}
ref={rg}
overscanRowCount={overscanRowCount}
scrollingResetTimeInterval={0}
onScroll={onChildScroll}
columnWidth={Math.floor(width / columnCount)}
columnCount={columnCount}
rowCount={Math.ceil(
(!items ? overscanRowCount : items?.length) /
columnCount
)}
rowHeight={cache.rowHeight}
cellRenderer={(gridCellProps: GridCellProps) =>
cellRenderer(gridCellProps, columnCount)
}
onSectionRendered={({
rowStartIndex,
rowStopIndex,
columnStartIndex,
columnStopIndex
}: SectionRenderedParams) => {
const startIndex =
rowStartIndex * columnCount + columnStartIndex;
const stopIndex =
rowStopIndex * columnCount + columnStopIndex;
return onRowsRendered({ startIndex, stopIndex });
}}
/>
) : null
}
</ColumnSizer>
);
}}
</AutoSizer>
)}
</InfiniteLoader>
</div>
)}
</WindowScroller>
);
const shouldRenderLoader =
!(items && items.length === totalResults) &&
loadMoreItems &&
items &&
items.length > 0;
const renderBottom = () => {
if (shouldRenderLoader)
return (
<LoaderWrapper>
<PuffLoader color={"#000"} size={LOADER_SIZE} />
</LoaderWrapper>
);
return null;
};
return (
<>
{infiniteLoaderRender()}
{renderBottom()}
</>
);
};
export default InfiniteGrid;
As you can see from this video, when you scroll to the bottom, then attempt to scroll up, it shifts wildly. It should only move up a few pixels but jumps a few more pixels than I'd expect.
https://user-images.githubusercontent.com/20436343/169559360-71b21a58-eb49-43c4-ba35-189ecea6e305.mov
And this is immediately after scrolling up just a few pixels on my mouse wheel
Notice how Test 752596
is close to the bottom and with the scroll, I'd expect it just be a little higher on the screen but a whole other item seems to appear when I would not expect it to. It's around the 8 second mark in the video and seems a lot more obvious there.
Here's a CodeSandbox that replicates the issue
Is there something I can do to make this smoother?
The more you scroll down and back up again, the worse the jumping gets.
any solution?