Skip to content

Instantly share code, notes, and snippets.

@mrcleanandfresh
Last active May 17, 2021 16:53
Show Gist options
  • Save mrcleanandfresh/f0589418fa9418f2a72ff65c647ef59a to your computer and use it in GitHub Desktop.
Save mrcleanandfresh/f0589418fa9418f2a72ff65c647ef59a to your computer and use it in GitHub Desktop.
React Virtualized Infinite loader with List example using React Hooks
import faker from 'faker';
import _ from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Col, Row } from 'react-bootstrap';
import { AutoSizer, IndexRange, InfiniteLoader, List, ListRowProps } from 'react-virtualized';
import wait from 'waait';
import { SuperProps } from './super-props';
export interface SuperListProps {
/**
* Minimum number of rows to be loaded at a time. This property can be used to batch requests to reduce HTTP
* requests. Defaults to 10.
*/
batchSize?: number,
/**
* Threshold at which to pre-fetch data. A threshold X means that data will start loading when a user scrolls
* within X rows. Defaults to 15.
*/
scrollThreshold?: number,
/**
* Reset any cached data about already-loaded rows. This method should be called if any/all loaded data needs to be
* re-fetched (eg a filtered list where the search criteria changes).
*/
isLoadMoreCacheReset?: boolean,
}
const SuperListInfinite = ( props: SuperListProps ) => {
const [ list, setList ] = useState<any[]>( [] );
const [ count, setCount ] = useState<number>( 1 );
const [ rowCount, setRowCount ] = useState<number>( 1 );
// memorizes the next value unless the list or count changes.
const hasNext = useMemo<boolean>(() => {
return count > list.length;
}, [count, list]);
/**
* Is The Row loaded
*
* This function is responsible for tracking the loaded state of each row.
*
* We chose Boolean() instead of !!list[index] because it's more performant AND clear.
* See: https://jsperf.com/boolean-conversion-speed
*/
const isRowLoaded = useCallback( ( { index } ) => {
return Boolean( list[ index ] );
}, [ list ] );
/**
* Load More Rows Implementation
*
* Callback to be invoked when more rows must be loaded. It should implement the following signature:
* ({ startIndex: number, stopIndex: number }): Promise. The returned Promise should be resolved once row data has
* finished loading. It will be used to determine when to refresh the list with the newly-loaded data. This
* callback
* may be called multiple times in reaction to a single scroll event.
*
* We wrap it in useCallback because we don't want the method signature to change from render-to-render unless one
* of the dependencies changes.
*/
const loadMoreRows = useCallback(( { startIndex, stopIndex }: IndexRange ): Promise<any> => {
const batchSize = stopIndex - startIndex;
const offset = stopIndex;
if (batchSize !== 0 || offset !== 0) {
return new Promise<any>( ( resolve ) => {
wait( 500 ).then( () => {
const newList: any[] = [];
for ( let i = offset; i < batchSize; i++ ) {
newList.push( {
id: i + 1,
name: `${faker.name.firstName( i % 2 )} ${faker.name.lastName( i % 2 )}`,
title: faker.name.title().toString(),
date: faker.date.past().toDateString(),
version: faker.random.uuid().toString(),
color: faker.commerce.color(),
} );
}
const newLists = list.concat( newList.filter( ( newItem ) => {
return _.findIndex( list, ( item ) => item.id === newItem.id ) === -1;
} ) );
// If there are more items to be loaded then add an extra row to hold a loading indicator.
setRowCount( hasNext
? newLists.length + 1
: newLists.length );
setList( newLists );
resolve();
} );
} );
} else {
return Promise.resolve();
}
}, [list, hasNext, setList, setRowCount]);
/** Responsible for rendering a single row, given its index. */
const rowRenderer = useCallback(( { key, index, style }: ListRowProps ) => {
if ( !isRowLoaded( { index } ) ) {
return (
<Row key={key} style={style}>
<Col xs={12}><span className="text-muted">Loading...</span></Col>
</Row>
);
} else {
return (
<Row key={key} style={style}>
<Col xs={1}><strong>Id</strong>: {list[ index ].id}</Col>
<Col xs={2}><strong>Name</strong>: {list[ index ].name}</Col>
<Col xs={2}><strong>Title</strong>: {list[ index ].title}</Col>
<Col xs={2}><strong>Updated</strong>: {list[ index ].date}</Col>
<Col xs={2}><strong>Version</strong>: {list[ index ].version}</Col>
<Col xs={3}><strong>Color</strong>: {list[ index ].color}</Col>
</Row>
);
}
}, [list, isRowLoaded]);
/** This effect will run on mount, and again only if the batch size changes. */
useEffect( () => {
wait( 500 ).then( () => {
const newList: any[] = [];
let batchSize;
if ( props.batchSize !== undefined ) {
batchSize = props.batchSize;
} else {
batchSize = 50;
}
for ( let i = 0; i < batchSize; i++ ) {
newList.push( {
id: i + 1,
name: `${faker.name.firstName( i % 2 )} ${faker.name.lastName( i % 2 )}`,
title: faker.name.title().toString(),
date: faker.date.past().toDateString(),
version: faker.random.uuid().toString(),
color: faker.commerce.color(),
} );
}
setList( newList );
setCount( 10000 );
} );
}, [ props.batchSize, hasNext ] );
/** If there are more items to be loaded then add an extra row to hold a loading indicator. */
useEffect(() => {
setRowCount( hasNext
? list.length + 1
: list.length );
}, [hasNext, list, setRowCount]);
return (
<InfiniteLoader
threshold={props.scrollThreshold}
isRowLoaded={isRowLoaded}
loadMoreRows={loadMoreRows}
rowCount={count}
>
{( { onRowsRendered, registerChild } ) => (
<AutoSizer disableHeight>
{( { width } ) => (
<List
height={500}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={rowCount}
rowHeight={100}
rowRenderer={rowRenderer}
width={width}
/>
)}
</AutoSizer>
)}
</InfiniteLoader>
);
};
export default SuperListInfinite;
@s-no1ukno
Copy link

Clean and well documented - thanks for throwing this up man!

@s-no1ukno
Copy link

One caveat I'd like to mention here before continuing, you have to give the table a fixed height! I found with React Virtualized you have to for whatever reason, give it a height prop to start with (which is required anyway) or else there's no height... therefore no scroll event, which means loadMoreRows doesn't fire.

For example's sake let's say I want to load 50 items at a time for whatever business reason. Alright, so when loadMoreRows is called, then I have already loaded 50 items from the server. So in loadMoreRows I want to start loading after item 50 because on the client I've already cached 1-50 from the server. That was our strategy. That way, the next loadMoreRows call will be 101-150, then 151-200, then 201-250, etc. This is why I use the list.length + 1 in the set row count. Whenever you have n more than your list length, React Virtualized fills n with placeholders; then, once the user scrolls within a certain buffer/threshold to those placeholders the loadMoreRows method gets fired.

How does the passed scrollThreshold prop relate to this? Maybe I'm missing it, but I don't see you actually using the scrollThreshold anywhere to be able tell it when to start loading the new list items.

@mrcleanandfresh
Copy link
Author

mrcleanandfresh commented May 17, 2021

How does the passed scrollThreshold prop relate to this?

You're not missing anything, it's in the interface declaration, but not being put inside the InfiniteScroll component in the Gist, just an oversight on my part in writing the Gist. But it would map to threshold I believe. It's been a long time since I wrote this Gist. The docs for InfiniteScroll go into slightly more detail on it, but it's pretty much what I copied/pasted above the interface prop value. It basically tells ReactVirtualized the offset from the bottom (of the previously loaded dataset) for which you want to start loading new data, at least that's the way I understood it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment