/ React

PrimeReact lazy table filtering

4youngpadawans.com presents PrimeReact lazy table filtering featuring React | PrimeReact | Spring


In rapidly evolving React ecosystem it is more and more hard to choose React UI component library that is the best fit for your or your clients' requirements: not because there are too few libraries but because there are too many and all of them look so promising.

When I was starting to learn about React ways, I wanted to choose UI library that has strong data table support because I was mostly developing business oriented web apps in closed environments.
Futher more, my Java JSF background and extensive usage of PrimeFaces JSF components both led me to PrimeReact, one of the youngest React component libraries on the market.

Since PrimeFaces has very good data table implementation, I decided to try PrimeReact implementation and to make it work with my Spring web service.

Basics

Lets suppose that we have a database table that contains thousands of records and intend to show all those records inside UI data table view.

The easiest way would be to load them all from database and flush them into UI via web service but that approach would have great negative impact on system performance with final result that loading of our web page containing table with thousands of records would be very slow.

In order to solve this problem and give a speed boost to our web app, it is recommended to utilize pagination with lazy loading technique provided by PrimeReact data table

  • requesting smaller subsets (pages) of our big data from web service,
  • loading any page on request

Front-end lazy pagination

To pull and retrieve only limited set of records from web service (page), PrimeReact data table provides following search criteria conditions

  • current page number,
  • page size (number of records per page),
  • (optional) filter criteria for table columns,
  • (optional) sorting columns in ascending or descending order

After user interacts with data table and changes any of value above, data table is able to pass search criteria to web service to get required data set.

Back-end lazy pagination

Our web service should be able to

  • accept and parse search criteria defined in data table,
  • get subset of records from database based on search criteria,
  • send subset back to data table

Front-end

Before going to next steps, lets suppose that

  • we've already created React project and added PrimeReact components to the project,
  • data table row is represented by following model (car and color), e.g
{
    id: 1,
    model: "Passat",
    year: 2001,
    brand: "VW",
    color: {id: 1, name: "Red", code: "#FF0000"}
}

Data table setup

Lets first define React component containing data table and understand how does it work.

import React, { Component } from 'react';
import { DataTable } from 'primereact/components/datatable/DataTable';
import { Column } from 'primereact/components/column/Column';

export class CarLazyTable extends Component {
    constructor() {
        super();
        //setting initial state values 
        this.state = { items: [], loading: false};
    }

    onLazyLoad = (event) => {
        console.log("On Lazy load event", event);
        //implementation goes here
    }

    onModelFilterChange = (event) => {
        console.log(event);
        //implementation goes here
    }

    colorColumnTemplate(rowData, column) {
        //car color column template goes here
    }

    render() {
        let modelFilter = <InputText style={{ width: '100%' }} className="ui-column-filter"
            onChange={this.onModelFilterChange} />

        let tableHeader = <div/>
      
        let datatable = <DataTable ref={(el) => this.dt = el} value={this.state.items}
            lazy={true} onLazyLoad={this.onLazyLoad}
            paginator={true} rows={5} rowsPerPageOptions={[5, 10, 20]} totalRecords={this.state.totalRecords}
            header={tableHeader}>
            <Column field="model" header="Model" sortable={true}
                filter={true} filterElement={modelFilter} filterMatchMode="contains" />
            <Column field="year" header="Year" sortable={true} />
            <Column field="brand" header="Brand" sortable={true} />
            <Column header="Color" body={this.colorColumnTemplate} />
        </DataTable>;

        return (
            <div>
                {datatable}
            </div>
        );
    }
}

The most important properties of <DataTable../> component are

  • value - array of records for one page,
  • totalRecords - total number of records from big data set,
  • lazy - puts data table in lazy mode
  • onLazyLoad - function called each time when data table requests for new page:
    • when component mounts,
    • when some of search criteria is changed
  • header - defines layout of data table header

Each data table column is defined with <Column../> component with following properties

  • field - field name from our record model,
  • header - column header title
  • sortable - enables column sorting,
  • filter - enables column filtering,
  • filterElement - defines layout and functionality of column filter input

Lazy load function

Each time data table requests for page, it calls onLazyLoad function with event argument containing following fields

  • filters - key-value map of column filters
  • first - page number
  • multiSortMeta - key-value map if sorting is enabled for multiple columns simultaneously,
  • rows - number of rows per one page,
  • sortField - sort field name
  • sortOrder - sort order (1-ascending, -1 descending) of sort field
    onLazyLoad = (event) => {
        console.log("On Lazy load event", event);
        let self = this;
        this.setState({ loading: true });
        this.carservice.getCarsLazy(event)
            .then(function (resItems) {
                console.log("Headers", resItems.headers);
                //get total record count from response header
                var totalRecords = resItems.headers['x-result-count'];
                //load items into local array
                self.setState({ totalRecords: Number(totalRecords), loading: false, items: resItems.data });
            }).catch(function (error) {
                console.log(error);
            });
    }

Passing search criteria to web service

When onLazyLoad function is triggered, we need to call web service using GET request (to be compliant with REST recommendations) and pass event argument to it.
But how can we send GET request without using request body in case when event argument is complex and contains fields and key-value maps?

There are several ways to do it but the easiest one is

  • to convert event object to JSON string,
  • to encode JSON string to Base64 and pass it as GET request's query string
import service from './service.jsx';
import axios from 'axios';

export class CarService {
    getCarsLazy(event) {
        console.log("Table lazy event",event);
        var filterJsonString=JSON.stringify(event);
        console.log("Filter",filterJsonString);
        //convert JSON string to Base64
        var filterJsonStringBase64=btoa(unescape(encodeURIComponent(filterJsonString)));
        //pass it to GET request as query string
        return service.getRestClient().get('cars?filter=' + filterJsonStringBase64);
    }
}

Column filter setup

For car model column we defined filter with input text layout.
Each time user types character into input text, function onModelFilterChange is called triggering onLazyLoad function.
Since the best practice would be to allow user to type few characters before web service is called, we can introduce delayed function call using timer.

    onModelFilterChange = (event) => {
        console.log(event);
        if (this.state.filterTimerId) {
            clearTimeout(this.state.filterTimerId);
        }
        let context = this;
        let filterValue = event.target.value;
        var filterTimerId = setTimeout(() => {
            //following line sets defined filter and triggers onLazyLoad function
            context.dt.filter(filterValue, 'model', 'contains');
            context.setState({ filterTimerId: null });
        }, 1000);
        this.setState({ filterTimerId: filterTimerId });
    }

When user types first character, we schedule context.dt.filter function execution with 1 second delay. If, in 1 second period, user types another character, we cancel timer and reschedule it.

Column layout template

Table columns can be styled to highlight specific table cells.

    colorColumnTemplate(rowData, column) {
        if (rowData['color']) {
            return <span style={{ color: rowData['color'].code }}>{rowData['color'].name}</span>;
        } else {
            return <span />;
        }
    }

As you can see, if rowData contains field color then we will show styled color name like this: Red, Green or Blue.

Table header template

Table header can be used to place additional functionalities like action buttons. For example, refresh, CRUD or loading/progress indicator

let tableHeader = <div className="ui-helper-clearfix" style={{ width: '100%' }}>
            <label style={{ float: 'left', fontWeight: 'bold' }}>PrimeReact lazy table with filtering and sorting</label>
            {this.refreshButton()}
        </div>

where refreshButton() function can be implemented like this

    refreshButton() {
        if (this.state.loading) {
            return <span style={{ float: 'right' }} className="fa fa-spinner fa-pulse fa-2x fa-fw" />
        } else {
            return <Button style={{ float: 'right' }} icon="fa-refresh" disabled={this.state.loading ? "disabled" : ""} />
        }
    }

In this example, while onLazyLoad function is in executing FontAwesome spinner is visible.

Back-end

UI is ready now. We can start creating back-end web service endpoint using Java Spring framework.

Lets first define 2 helper classes so that we can utilize Gson library and easily convert JSON from GET request query string to Java object.

LazyPageParamRest.java
//this class is used to easily map search criteria from PrimeReact data table
public class LazyPageParamsRest {
    //page number
    Integer first;
    //rows per page
    Integer rows;
    //name of field to sort
    String sortField;
    Integer sortOrder;
    //field filters
    Map<String, LazyPageFilter> filters;
    
    //getters and setters...
}
LazyPageFilter.java
public class LazyPageFilter {
    String value;
    String matchMode;
    
    //getters and setters
}

Finally, /cars endpoint can be implemented like this (check out code comments for additional explanations)

@Controller
@RequestMapping(path = "/cars")
public class CarRestController {
    @Autowired
    private CarRepository carRepository;
    @Autowired
    private CarService carService;

    @CrossOrigin(origins = "*")
    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity<?> getAll(@RequestParam(required = false, value = "filter") String filter) {
        //first we get total count of records from database
        long count = carRepository.count();
        //custom java class to store search related criteria
        GenericSearchCriteria gsc = new GenericSearchCriteria();
        if (filter != null) {
            //decode filter request param value from Base64
            byte[] decoded = Base64.getDecoder().decode(filter);
            String decodedStr = new String(decoded);
            System.out.println("Lazy search criteria: " + decodedStr);
            //convert JSON string to LazyPageParamsRest object using Gson library
            LazyPageParamsRest lppr = new Gson().fromJson(decodedStr, LazyPageParamsRest.class);
            
            if (lppr.getFirst() != null && lppr.getRows() != null) {
                gsc.setFirstRecord(lppr.getFirst());
                gsc.setPageSize(lppr.getRows());
            } else {
                gsc.setFirstRecord(0);
                gsc.setPageSize((int) count);
            }
            if (lppr.getSortField() != null) {
                gsc.setSortField(lppr.getSortField());
                if (lppr.getSortOrder() == 1) {
                    gsc.setSortOrder("ASCENDING");
                } else {
                    gsc.setSortOrder("DESCENDING");
                }
            }
            if (lppr.getFilters()!=null){
                //create filters map for generic search criteria
                Map<String,Object> searchCriteriaFilters=new HashMap<>();
                Map<String, LazyPageFilter> filters = lppr.getFilters();
                if (filters.containsKey("model")){
                    LazyPageFilter modelFilter=filters.get("model");
                    searchCriteriaFilters.put("model",modelFilter.getValue());
                    gsc.setFilters(searchCriteriaFilters);
                }
            }
        } else {
            gsc.setFirstRecord(0);
            gsc.setPageSize((int) count);
        }

        //get reguired page from our car data service based on search criteria
        List<Car> items = carService.getLazyData(gsc);
        HttpHeaders headers = new HttpHeaders();
        //put total record count into custom X-Result-Count header
        headers.add("X-Result-Count", String.valueOf(count));
        //allow browser to read X-Result-Count header
        headers.add("Access-Control-Expose-Headers", "X-Result-Count");

        //return response with required car page
        return ResponseEntity.ok().headers(headers).body(items);
    }

Result

primereact_lazy_table

PrimeReact lazy table filtering
Share this