wavematt
wavematt

Reputation: 418

Infinite loading Grid is triggering loadMoreRows at the wrong time

I am using react-virtualized to build an infinite loading grid, which also uses AutoSizer to handle a dynamic grid width and WindowScroller to enable scrolling with the viewport. However, I am not able to successfully trigger loadMoreRows at the appropriate point in my infinite loading Grid.

I've narrowed down two things:

I put together a minimal plunker demo which will render a 3-column grid of random "lorem ipsum" text snippets. There is no end point for this example content, it just loads as much as you will scroll.

Plunker Demo: http://plnkr.co/edit/uoRdanlB1rXsgBe2Ej8i

import React, { Component } from 'react';
import { render } from 'react-dom';
import { AutoSizer, CellMeasurer, CellMeasurerCache, Grid, InfiniteLoader, WindowScroller } from 'react-virtualized';

const MIN_BATCH_SIZE = 40;

// Return random snippet of lorem ipsum text
const randText = () => {
    const text = [
        'Lorem ipsum dolor sit amet.',
        'Consectetur adipisicing elit.',
        'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
        'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
        'Ut enim ad minim veniam.',
        'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
        'Excepteur sint occaecat cupidatat non proident.',
        'Sunt in culpa qui officia deserunt mollit anim id est laborum.',
        'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
    ];

    return text[Math.floor(Math.random() * text.length)];
};

// Cell data
const list = [];


// -----------------------------------------------------------------------------


// Infinite loading Grid that is AutoSize'd and WindowScroll'd with dynamic cell heights
class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            columnWidth: 300,
            columnCount: 3,
            rowCount: 0,
            isLoading: false
        };

        this._cache = new CellMeasurerCache({
            fixedWidth: true,
            defaultHeight: 30
        });

        this._cellRenderer = this._cellRenderer.bind(this);
        this._isRowLoaded = this._isRowLoaded.bind(this);
        this._loadMoreRows = this._loadMoreRows.bind(this);
        this._onResize = this._onResize.bind(this);
        this._onSectionRendered = this._onSectionRendered.bind(this);
    }

    componentDidMount() {
        this.setState({ rowCount: 1 });
    }

    componentWillUpdate(nextProps, nextState) {
        const { columnCount, rowCount } = this.state;

        if (rowCount !== nextState.rowCount) {
            if (nextState.rowCount > rowCount) {
                // Re-measure the row at the index which was last occupied by "loading" content
                for (let i = 0; i < columnCount; i++) {
                    this._cache.clear(this._lastLoadingIndex, i);
                }
            }
        }
    }

    render() {
        const { columnCount, columnWidth, rowCount } = this.state;

        return (
            <div className="container-fluid">
                <h1 className="page-header lead">RV Infinite Grid</h1>

                <InfiniteLoader
                    isRowLoaded={this._isRowLoaded}
                    loadMoreRows={this._loadMoreRows}
                    rowCount={rowCount}
                    threshold={5}
                >
                    {({ onRowsRendered, registerChild }) => {
                        this._onRowsRendered = onRowsRendered;

                        return (
                            <WindowScroller>
                                {({ height, scrollTop }) => (
                                    <AutoSizer
                                        disableHeight
                                        onResize={this._onResize}
                                    >
                                        {({ width }) => (
                                            <Grid
                                                autoHeight
                                                width={width}
                                                height={height}
                                                scrollTop={scrollTop}

                                                ref={grid => {
                                                    this._grid = grid;
                                                    registerChild(grid);
                                                }}

                                                columnWidth={columnWidth}
                                                columnCount={columnCount}

                                                rowCount={rowCount}
                                                rowHeight={this._cache.rowHeight}

                                                cellRenderer={this._cellRenderer}
                                                onSectionRendered={this._onSectionRendered}
                                            />
                                        )}
                                    </AutoSizer>
                                )}
                            </WindowScroller>
                        );
                    }}
                </InfiniteLoader>
            </div>
        );
    }

    _isRowLoaded({ index }) {
        const { rowCount } = this.state;

        return index < rowCount - 1;
    }

    _loadMoreRows({ startIndex, stopIndex }) {
        const { isLoading } = this.state;
        const delay = 100 + Math.floor(Math.random() * 3000); // random delay to simulate server response time

        if (!isLoading) {
            this.setState({
                isLoading: true
            });

            setTimeout(() => {
                // Generate some new rows (for this example, we have no actual end point)
                for (let i = 0; i < MIN_BATCH_SIZE; i++) {
                    list.push([ randText(), randText(), randText() ]);
                }

                // Cancel the "loading" state and update the`rowCount`
                this.setState({
                    isLoading: false,
                    rowCount: list.length + 1
                }, done);
            }, delay);

            let done;
            return new Promise(resolve => done = resolve);
        }
    }

    _cellRenderer({ key, rowIndex, columnIndex, parent, style }) {
        const { columnCount, columnWidth, rowCount } = this.state;
        let content;

        // Render cell content
        if (rowIndex < rowCount - 1) {
            const cellStyle = Object.assign({}, style, {
              backgroundColor: (rowIndex % 2 ? null : '#eee')
            });

            content = (
                <div style={cellStyle}>
                    <div style={{ padding: '20px' }}>
                        {list[rowIndex][columnIndex] || <em className="text-muted">empty</em>}
                    </div>
                </div>
            );
        }

        // Render "loading" content
        else if (columnIndex === 0) {
            // Remember this `index` so we can clear its measurements from the cache later
            this._lastLoadingIndex = rowIndex;

            const cellStyle = Object.assign({}, style, {
              width: (columnWidth * columnCount), // Give loader the full grid width
              textAlign: 'center'
            });

            content = <div style={cellStyle}>Loading...</div>;
        }

        // Render empty cell (for incomplete rows)
        else {
            content = <div style={style} />;
        }

        return (
            <CellMeasurer
                key={key}
                cache={this._cache}
                parent={parent}
                columnIndex={columnIndex}
                rowIndex={rowIndex}
            >
                {content}
            </CellMeasurer>
        );
    }

    _onResize({ width }) {
        this.setState({
            // Subtracting 30 from `width` to accommodate the padding from the Bootstrap container
            columnWidth: (width - 30) / 3
        });

        this._cache.clearAll();
        this._grid.recomputeGridSize();
    }

    _onSectionRendered({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) {
        const { columnCount } = this.state;

        const startIndex = rowStartIndex * columnCount + columnStartIndex;
        const stopIndex = rowStopIndex * columnCount + columnStopIndex;

        this._onRowsRendered({
            startIndex,
            stopIndex
        });
    }
}


render(<App />, document.getElementById('root'));

Upvotes: 2

Views: 2345

Answers (1)

wavematt
wavematt

Reputation: 418

Answering my own question...

It turns out that my _onSectionRendered function was providing the wrong range to my _onRowsRendered function. I'll admit that I blindly copied and pasted the example snippet from the InfiniteLoader docs into my own project until I understood more about it. It made sense to me at first to return the range of cells the way that it does in the docs, until I reminded myself that InfiniteLoader is looking at rows, not cells.

The example in the docs sets startIndex and stopIndex to a range of cells rather than returning the range of rows.

_onSectionRendered ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }) {
  const startIndex = rowStartIndex * columnCount + columnStartIndex
  const stopIndex = rowStopIndex * columnCount + columnStopIndex

  this._onRowsRendered({
    startIndex,
    stopIndex
  })
}

To solve my issue, I only pass rowStartIndex and rowStopIndex to onRowsRendered().

onSectionRendered({ rowStartIndex, rowStopIndex }) {
  this._onRowsRendered({
    startIndex: rowStartIndex,
    stopIndex: rowStopIndex
  })
}

This is updated in my plunker example.

Upvotes: 3

Related Questions