DataTable using lazy/virtual scrolling with sort & filter
Posted: 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 -
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.
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"
]
}
}