Reputation: 1455
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;
And 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.
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?
Upvotes: 3
Views: 1487
Reputation: 4082
I get an improvement when I delete <img src={image} alt="test" />
. I see that in the network tab, the images get reloaded when going up. If you look at infinite scrolls like twitter or reddit, the content above get partially unloaded, but the layout stays. So that it doesn't mess up document height.
That mean, once you loaded the image, you should set the image container size to the size of the image, so that when the image is unloaded, the layout stays the same above scroll position.
Careful with putting height: 500px; max-height: 500px
, this won't be enough to fix the height, if you have padding or margin on your list elements, this will impact the list itself. Example: padding 1000px on image, will make your list element bigger, even if you put your list element height to 400px. Purely in CSS this can -somewhat- be fixed with overflow: hidden
, but all this may mess up calculations.
It is kinda the same with margin, there is a place where you put margin: 50px auto
, two div above, the height of the div is bigger than the colored rectangle you see on the view.
Each time I have bump when scrolling down, I see "clap" being logged. Suspicious.
useEffect(() => {
console.log("clap");
cache.clearAll();
if (loaderRef && loaderRef.current) {
loaderRef.current.resetLoadMoreRowsCache(true);
}
}, [loaderRef, cache, items]);
Also, for anyone use codesandbox, make sure to reload the page, no just the iframe, otherwise you get errors like : Children cannot be added or removed during a reorder operation.
.
When I scroll up, I sometimes get slightly pushed back to the bottom. Maybe something is being loaded upside scroll bar and change document height ?
Also you should avoid using this pattern:
const infiniteLoaderRender = () => (<span/>)
return (
{infiniteLoaderRender()}
)
Simplify, and avoid unecesary re-render:
const infiniteLoaderRender = <span/>
return (
{infiniteLoaderRender}
)
This does seem to improve the scrolling a lot. But not sure it fixe it.
If you can, instead of populating the list with random elements, make a fixed list version, so that we can reproduce the bug easily.
Upvotes: 1