DEV Community

Garry Xiao
Garry Xiao

Posted on • Edited on

React infinite loader with TypeScript

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> ) } 
Enter fullscreen mode Exit fullscreen mode

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}/> 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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> ) }) 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)