Last active
May 17, 2021 16:53
-
-
Save mrcleanandfresh/f0589418fa9418f2a72ff65c647ef59a to your computer and use it in GitHub Desktop.
React Virtualized Infinite loader with List example using React Hooks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
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
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.