Virtual Scrolling

Virtual scrolling provides an alternative to paging.

While the user is scrolling the table, the Grid requests and displays only the visible pages.

Getting Started

To set up the Grid for virtual scrolling:

  1. Set its height either through its style property.
  2. Set the regular or detail row height and the number of records.
  3. Provide the data for each page through the onPageChange event handler.

To work properly, virtual scrolling requires you to set the following configuration options:

  • (Required) scrollable—Set it to virtual.
  • (Required) height through style
  • (Required) skip
  • (Required) total
  • (Required) pageSize—To avoid unexpected behavior during scrolling, set pageSize to at least the number of the visible Grid elements. The number of the visible Grid elements is determined by the height and rowHeight settings of the Grid.
  • (Required) data
  • (Optional) rowHeight
import React from 'react';
import ReactDOM from 'react-dom';
import { Grid, GridColumn as Column } from '@progress/kendo-react-grid';

class App extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            data: this.createRandomData(50000),
            skip: 0
        };
        this.pageChange = this.pageChange.bind(this);
    }

    pageChange(event) {
        this.setState({
            orders: this.state.orders,
            skip: event.page.skip
        });
    }

    /* Generating example data */
    createRandomData(count) {
        const firstNames = [ 'Nancy', 'Andrew', 'Janet', 'Margaret', 'Steven', 'Michael', 'Robert', 'Laura', 'Anne', 'Nige' ],
            lastNames = [ 'Davolio', 'Fuller', 'Leverling', 'Peacock', 'Buchanan', 'Suyama', 'King', 'Callahan', 'Dodsworth', 'White' ],
            cities = [ 'Seattle', 'Tacoma', 'Kirkland', 'Redmond', 'London', 'Philadelphia', 'New York', 'Seattle', 'London', 'Boston' ],
            titles = [ 'Accountant', 'Vice President, Sales', 'Sales Representative', 'Technical Support', 'Sales Manager', 'Web Designer',
                'Software Developer' ];

        return Array(count).fill({}).map((_, idx) => ({
            id: idx + 1,
            firstName: firstNames[Math.floor(Math.random() * firstNames.length)],
            lastName: lastNames[Math.floor(Math.random() * lastNames.length)],
            city: cities[Math.floor(Math.random() * cities.length)],
            title: titles[Math.floor(Math.random() * titles.length)]
        }));
    }

    render() {
        return (
            <Grid
                style={{ height: '440px' }}
                rowHeight={40}
                data={this.state.data.slice(this.state.skip, this.state.skip + 20)}
                pageSize={20}
                total={this.state.data.length}
                skip={this.state.skip}

                scrollable={'virtual'}
                onPageChange={this.pageChange}
            >
                <Column field="id" title="ID" width="70px" />
                <Column field="firstName" title="First Name" />
                <Column field="lastName" title="Last Name" />
                <Column field="city" title="City" width="120px" />
                <Column field="title" title="Title" width="200px" />
            </Grid>
        );
    }
}

ReactDOM.render(
    <App />,
    document.querySelector('my-app')
);

Using Virtualization with Grouping

You can use virtual scrolling in combination with grouped data.

  1. Set the groupable and group options of the Grid.

  2. Set the scrollable option to virtual.

  3. Handle the emitted onDataStateChange event. The onDataStateChange event fires upon user interaction with the scrolling or changing the groups, and then processes the data and returns the requested page to the Grid.

    To programmatically implement the processing of the data, either:

  • Send a request to the server to execute the grouping for the current page on the server side, or
  • Use the process method of the DataQuery library which automatically processes the data.

The Grid expects the grouped data to be a collection of GroupResults.

import React from 'react';
import ReactDOM from 'react-dom';

import { Grid, GridColumn as Column } from '@progress/kendo-react-grid';
import { process } from '@progress/kendo-data-query';

import products from './products.json';

class App extends React.PureComponent {
    state = this.createAppState({
        take: 15,
        group: [
            { field: 'UnitsInStock' },
            { field: 'Category.CategoryName' }
        ]
    });

    render() {
        return (
            <Grid
                style={{ height: '520px' }}
                resizable reorderable filterable sortable groupable
                scrollable="virtual"
                data={this.state.result}
                onDataStateChange={this.dataStateChange}
                {...this.state.dataState}
            >
                <Column field="ProductID" filterable={false} title="ID" width="50px" />
                <Column field="ProductName" title="Product Name" />
                <Column field="UnitPrice" title="Unit Price" filter="numeric" />
                <Column field="UnitsInStock" title="Units In Stock" filter="numeric" />
                <Column field="Category.CategoryName" title="Category Name" />
            </Grid>
        );
    }

    createAppState(dataState) {
        return {
            result: process(products, dataState),
            dataState: dataState
        };
    }

    dataStateChange = (event) => {
        this.setState(this.createAppState(event.data));
    }
}

ReactDOM.render(<App />, document.querySelector('my-app'));

Debouncing pageChange Events

When configured for virtualization, the Grid fires the onPageChange event as often as possible. This behavior allows for a smoother scrolling experience when the data is available in memory.

If the data is requested from a remote service, it is advisable to debounce or otherwise limit the page changes.

import React from 'react';
import ReactDOM from 'react-dom';
import { Grid, GridColumn as Column } from '@progress/kendo-react-grid';

class App extends React.Component {
    pageSize = 20;
    baseUrl = `https://demos.telerik.com/kendo-ui/service-v4/odata/Orders?$count=true&$top=60&$skip=`;
    init = { method: 'GET', accept: 'application/json', headers: {} };

    constructor(props) {
        super(props);
        this.state = {
            orders: [],
            total: 0,
            skip: 0
        };
    }

    render() {
        return (
            <Grid
                style={{ height: '440px' }}
                rowHeight={40}
                data={this.state.orders.slice(this.state.skip, this.state.skip + 20)}
                pageSize={this.pageSize}
                total={this.state.total}
                skip={this.state.skip}

                scrollable={'virtual'}
                onPageChange={this.pageChange}
                cellRender={this.loadingCell}
            >
                <Column field="Index" title="Index" width="70px" />
                <Column field="OrderID" title="Order Id" width="100px" />
                <Column field="ShipCountry" title="Ship Country" />
                <Column field="ShipName" title="Ship Name" />
            </Grid>
        );
    }

    componentDidMount() {
        // request the first page on initial load
        this.requestData(0);
    }

    requestIfNeeded(skip) {
        for (let i = skip; i < skip + this.pageSize && i < this.state.orders.length; i++) {
            if (this.state.orders[i].OrderID === undefined) {
                // request data only if not already fetched
                this.requestData(skip);
                return;
            }
        }
    }

    requestData(skipParameter) {
        if (this.requestInProgress) {
            //perform only one request at a time
            return;
        }
        this.requestInProgress = true;

        const skip = Math.max(skipParameter - this.pageSize, 0); //request the prev page as well
        fetch(this.baseUrl + skip, this.init)
            .then(response => response.json())
            .then(json => {
                this.requestInProgress = false;

                const total = json['@odata.count'];
                const data = json['value'];

                const orders = this.state.orders.length === total ?
                    this.state.orders :
                    new Array(total).fill().map((e, i) => ({ Index: i }));

                data.forEach((order, i) => {
                    orders[i + skip] = { Index: i + skip, ...order };
                });

                this.requestIfNeeded(this.state.skip);
                this.setState({
                    orders: orders,
                    total: total
                });
            });
    }

    pageChange = (event) => {
        this.requestIfNeeded(event.page.skip);
        this.setState({
            skip: event.page.skip
        });
    }

    loadingCell = (tdElement, props) => {
        if (props.dataItem[props.field] === undefined) {
            // shows loading icon if no data
            return <td> <span className="k-icon k-i-loading"></span></td>;
        }

        // default rendering for this cell
        return tdElement;
    };
}

ReactDOM.render(
    <App />,
    document.querySelector('my-app')
);
 /