React infinite loader with TypeScript
Garry Xiao
Posted on June 4, 2020
It's a performance favor solution for a list or grid of huge data. There are limited TypeScript examples to work with 'react-window-infinite-loader' and 'react-window' (A new package for react-virtualized, 'Virtualized Table', https://material-ui.com/components/tables/). I will create a functional component to work through these.
For react-window
https://github.com/bvaughn/react-window/
For react-window-infinite-loader:
https://github.com/bvaughn/react-window-infinite-loader
Encapsulated into a seperated component, reduced unnecessary properties provided with the sample:
import React, { ComponentType } from 'react'
import { FixedSizeList, ListChildComponentProps, Layout, ListOnScrollProps, ListItemKeySelector } from 'react-window'
import InfiniteLoader from 'react-window-infinite-loader'
import { ISearchItem } from '../views/ISearchResult'
/**
* List item renderer properties
*/
export interface ListItemRendererProps extends ListChildComponentProps {
}
/**
* Infinite list props
*/
export interface InfiniteListProps {
/**
* Is horizontal layout
*/
horizontal?: boolean
/**
* Height
*/
height?: number
/**
* Inital scroll offset, scrollTop or scrollLeft
*/
initialScrollOffset?: number
/**
* Item unit property name, default is id
*/
itemKey?: string
/**
* Item renderer
* @param props
*/
itemRenderer(props: ListItemRendererProps): React.ReactElement<ListItemRendererProps>
/**
* Item size (height)
*/
itemSize: number
/**
* Load items callback
*/
loadItems(page: number, records: number): Promise<ISearchItem[]>
/**
* On scroll callback
*/
onScroll?: (props: ListOnScrollProps) => any
/**
* Records to read onetime
*/
records: number
/**
* Width
*/
width?: string
}
/**
* Infinite list state class
*/
class InfiniteListState {
/**
* List items
*/
items: ISearchItem[]
/**
* All data is loaded
*/
loaded: boolean
/**
* Current page
*/
page: number
/**
* Constructor
* @param items Init items
*/
constructor(items: ISearchItem[]) {
this.items = items
this.loaded = false
this.page = 0
}
}
/**
* Infinite list component
* @param pros Properties
*/
export function InfiniteList(props: InfiniteListProps) {
// Items state
const [state, updateState] = React.useState(new InfiniteListState([]))
// Render an item or a loading indicator
const itemRenderer: ComponentType<ListChildComponentProps> = (lp) => {
const newProps: ListItemRendererProps = {
data: state.items[lp.index],
index: lp.index,
isScrolling: lp.isScrolling,
style: lp.style,
}
return props.itemRenderer(newProps)
}
// Determine the index is ready
const isItemLoaded = (index: number) => {
return state.loaded || index < state.items.length
}
// Load more items
const loadMoreItems = async (startIndex: number, stopIndex: number) => {
// Loaded then return
if(state.loaded)
return
// Read next page
const page = state.page + 1
const items = (await props.loadItems(page, props.records)) || []
// Add to the collection
state.items.push(...items)
// New state
const newState = new InfiniteListState(state.items)
newState.page = page
newState.loaded = items.length < props.records
// Update
updateState(newState)
}
// Add 1 to the length to indicate more data is available
const itemCount = state.items.length + (state.loaded ? 0 : 1)
// Default calcuated height
const height = props.height || props.records * props.itemSize
// Default 100% width
const width = props.width || '100%'
// Layout
const layout: Layout = props.horizontal ? 'horizontal' : 'vertical'
// Item key
const itemKey: ListItemKeySelector = (index, data) => {
const field = props.itemKey || 'id'
if(data == null || data[field] == null)
return index
return data[field]
}
// Return components
return (
<InfiniteLoader isItemLoaded={isItemLoaded} itemCount={itemCount} loadMoreItems={loadMoreItems} minimumBatchSize={props.records} threshold={props.records + 5}>
{
({ onItemsRendered, ref }) => (
<FixedSizeList itemCount={itemCount}
onItemsRendered={onItemsRendered}
onScroll={props.onScroll}
ref={ref}
layout={layout}
itemKey={itemKey}
initialScrollOffset={props.initialScrollOffset}
itemSize={props.itemSize}
width={width}
height={height}
>{itemRenderer}</FixedSizeList>
)
}
</InfiniteLoader>
)
}
An example to use it:
// Load datal
const loadItems = async (page: number, records: number) => {
const conditions: CustomerSearchModel = { page, records }
return (await api.searchPersonItems(conditions)).items
}
// Item renderer
const itemRenderer = (props: ListItemRendererProps) => {
return (
<div className={classes.listItem} style={props.style}>{props.index} {props.data == null ? 'Loading...' : props.data['name']}</div>
)
}
<InfiniteList itemSize={200} records={5} height={height} loadItems={loadItems} itemRenderer={itemRenderer}/>
Here the property 'height' of the component in vertical case is not easy to dertermine. The full height of the document, minus the app bar height, margin or padding height, is the target height. I coded a hook to calculate two elements at the same time for calculate the real height with
// Calculate dimensions, pass ref1 to AppBar (position="sticky"), ref2 to the outer Container
const {ref1, ref2, dimensions1, dimensions2} = useDimensions2<HTMLElement, HTMLDivElement>(true)
// Setup the actual pixel height
const mainStyle = {
height: (dimensions1 && dimensions2 ? (dimensions2.height - dimensions1.height) : 0)
}
/**
* Calculate 2 elements dimensions
* @param observeResize Is observing resize event
*/
export function useDimensions2<E1 extends Element, E2 extends Element>(observeResize: boolean = false) {
// References for a HTML elements passed to its 'ref' property
const ref1 = React.useRef<E1>(null)
const ref2 = React.useRef<E2>(null)
// Dimensions and update state
const [dimensions, updateDimensions] = React.useState<DOMRect[]>()
// Calcuate when layout is ready
React.useEffect(() => {
// Update dimensions
if(ref1.current && ref2.current)
updateDimensions([ref1.current.getBoundingClientRect(), ref2.current.getBoundingClientRect()])
// Resize event handler
const resizeHandler = (event: Event) => {
if(ref1.current && ref2.current)
updateDimensions([ref1.current.getBoundingClientRect(), ref2.current.getBoundingClientRect()])
}
// Add event listener when supported
if(observeResize)
window.addEventListener('resize', resizeHandler)
return () => {
// Remove the event listener
if(observeResize)
window.removeEventListener('resize', resizeHandler)
}
}, [ref1.current, ref2.current])
// Dimensions
const dimensions1 = dimensions == null ? null : dimensions[0]
const dimensions2 = dimensions == null ? null : dimensions[1]
// Return
return {
ref1,
ref2,
dimensions1,
dimensions2
}
}
There are two additional interesting topics. How to add outer or inner elements add to the InfiniteList:
// Outer element
const outerElementType = React.forwardRef<HTMLElement>((p, ref) => {
return (
<Table innerRef={ref}>
{p.children}
</Table>
)
})
Posted on June 4, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.