DataTable using lazy/virtual scrolling with sort & filter

UI Components for React
Post Reply
jsaxis
Posts: 1
Joined: 05 Jul 2022, 15:45

06 Jul 2022, 21:17

I noticed an issue in my actual project when trying to use the DataTable component with virtual/lazy loading including filters and sorting on the backend via an odata endpoint. The table seems to fall out of sync when scrolling and interacting with the filter input (filterDisplay="row"). Sometimes it takes a few attempts to trigger the behavior, but it is pretty consistent - and I'm not sure if it's something specific to my code or a bug in the component(s).

To demonstrate the issue, I've combined and modified the filter demo + virtual demo here: https://www.primefaces.org/primereact/d ... ualscroll/

The carService.get was modified to take a filter string in addition to the first and last values from onLazyLoad.

Steps to recreate -
  • Create CRA app

    Code: Select all

    npx create-react-app prime-datatable-issue
  • Install prime pieces

    Code: Select all

    yarn add primereact primeflex primeicons
  • Overwrite App.js contents with the snippet below

    Code: Select all

    import "primeicons/primeicons.css";
    import "primereact/resources/themes/lara-light-indigo/theme.css";
    import "primereact/resources/primereact.css";
    import "primeflex/primeflex.css";
    
    import React, { useRef, useState } from "react";
    import { FilterMatchMode } from 'primereact/api';
    import { DataTable } from "primereact/datatable";
    import { Column } from "primereact/column";
    import { CarService } from "./carService";
    
    let renderCount = 0;
    const carService = new CarService();
    
    const DataTableVirtualScrollDemo = () => {
      const dtRef = useRef();
      const [virtualCars, setVirtualCars] = useState(Array.from({ length: 10000 }));
      const [lazyLoading, setLazyLoading] = useState(false);
      const [lazyParams, setLazyParams] = useState({
        filters: {
          'id': { value: null, matchMode: FilterMatchMode.CONTAINS }
        }
      });
    
      const loadCarsLazy = (params) => {
        console.log('loadCarsLazy: ', params);
        !lazyLoading && setLazyLoading(true);
    
        let { first, last, filters } = params;
        const filter = filters.id.value || "";
    
        //load data of required page
        const { value, count } = carService.get({ filter, first, last });
    
        setVirtualCars(() => {
          // the next lines resize the virtual data array as needed and merge previously
          // loaded data with the page that was returned on the current response
          const _prev = virtualCars.length === count ? [...virtualCars] : Array.from({ length: count });
          const _virtualData = [..._prev.slice(0, first), ...value, ..._prev.slice(last)];
          console.log("_virtualData: ", _virtualData);
          return _virtualData;
        });
        setLazyLoading(false);
      };
    
      console.log("renderCount: ", renderCount++);
    
      const onChangeLazyParams = (event) => {
        // if the filter changes, reset the scroll
        if (event.hasOwnProperty('filters')) {
          if (dtRef.current) dtRef.current.resetScroll();
        }
    
        // update lazyParams and fetch the appropriate data
        const next = { ...lazyParams, ...event };
        console.log('onChangeLazyParams: ', next);
        setLazyParams(next);
        loadCarsLazy(next);
      };
    
      return (
        <div className="mx-8">
          <div className="card">
            <h5>Lazy Loading from a Remote Datasource (1000 Rows)</h5>
            <DataTable ref={dtRef} value={virtualCars} lazy
              scrollable scrollHeight="400px"
              filterDisplay="row" filters={lazyParams.filters} onFilter={onChangeLazyParams}
              virtualScrollerOptions={{ lazy: true, onLazyLoad: onChangeLazyParams, itemSize: 48 }}>
              <Column field="id" header="Id" style={{ minWidth: "200px" }} 
                showFilterMenu={false} filter filterMatchMode="contains" />
              <Column field="vin" header="Vin" style={{ minWidth: "200px" }} />
              <Column field="year" header="Year" style={{ minWidth: "200px" }} />
              <Column field="brand" header="Brand" style={{ minWidth: "200px" }} />
              <Column field="color" header="Color" style={{ minWidth: "200px" }} />
            </DataTable>
          </div>
        </div>
      );
    };
    
    export default DataTableVirtualScrollDemo;
    
  • Add carService.js to the src/ folder

    Code: Select all

    export class CarService {
      cars = [];
      brands = ["Vapid", "Carson", "Kitano", "Dabver", "Ibex", "Morello", "Akira", "Titan", "Dover", "Norma"];
      colors = ["Black", "White", "Red", "Blue", "Silver", "Green", "Yellow"];
    
      constructor() {
        window.cars = this.cars = Array.from({ length: 10000 }).map((m, i) => this.generateCar(i));
      }
    
      generateCar(id) {
        return {
          id: id.toString(),
          vin: this.generateVin(),
          brand: this.generateBrand(),
          color: this.generateColor(),
          year: this.generateYear()
        };
      }
    
      generateVin() {
        let text = "";
        let possible =
          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    
        for (let i = 0; i < 5; i++) {
          text += possible.charAt(Math.floor(Math.random() * possible.length));
        }
    
        return text;
      }
    
      generateBrand() {
        return this.brands[Math.floor(Math.random() * Math.floor(10))];
      }
    
      generateColor() {
        return this.colors[Math.floor(Math.random() * Math.floor(7))];
      }
    
      generateYear() {
        return 2000 + Math.floor(Math.random() * Math.floor(19));
      }
    
      get({ filter, first, last }) {
        let value = [];
        let count = 0;
        let v = { value, count };
    
        if (filter.length) {
          value = this.cars.filter((f) => f.id.indexOf(filter) > -1);
          v = { value: value.slice(first, last), count: value.length };
        } else {
          value = this.cars.slice(first, last);
          v = { value, count: this.cars.length };
        }
    
        console.log(`carService.get({'${filter}', ${first}, ${last}}): `, v);
        return v;
      }
    }
    
    
  • Start App -

    Code: Select all

    npm run start
  • In the "Id" filter, type "123" (table filters as expected)
  • This may take a few tries - while scrolling the table with the filter still focused, add or delete the "3" from the filter value (table sometimes updates appropriately, other times it will remain on the previous filtered data set.
For reference, my package.json -

Code: Select all

{
  "name": "prime-datatable-issue",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^13.5.0",
    "primeflex": "^3.2.1",
    "primeicons": "^5.0.0",
    "primereact": "^8.2.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Melloware
Posts: 3247
Joined: 22 Apr 2013, 15:48

08 Jul 2022, 16:46

Most likely a bug.
PrimeFaces Developer | PrimeFaces Extensions Developer
GitHub Profile: https://github.com/melloware
PrimeFaces Elite 11.0.3 / PF Extensions 11.0.3
PrimeReact 8.0.0

Post Reply

Return to “PrimeReact”

  • Information
  • Who is online

    Users browsing this forum: No registered users and 3 guests