Метод фильтрации и сортировки массивов для передачи в компонент. React/Redux

Йо-йо! Всё моё время сейчас уходит на java script и react. Я также работаю в компании, которая продаёт автомобили онлайн. Недавно мы выкатили в «prod» новый дизайн списка сравнения. Прошлый генерировался с помощью php, а далее редактировался с помощью jQuery. Новый же написан на реакте. Плюс добавилась сортировка автомобилей.

Само собой я использую хранилище для данных, в том числе и списка автомобилей. Проблема в том, что если бы я сортировал и фильтровал данные в store, то либо постепенно при фильтрации данные из него удалялись, либо пришлось иметь два экземпляра списка ( полный и фильтрованный/сортированный ).

Сейчас я хочу показать как изящно избавился от этой проблемы, а ещё написал красивый метод для фильтрации и сортировки массивов.

Дано

import React, { Component } from 'react';
import TableColumns from './TableColumns'
import {connect} from "react-redux";
import _CTFI from '../../store/utils';



class TableColumnsContainer extends Component{
    render(){
        const {
            tabSign, // какой таб? это параметр для фильтрации. В моём случае это все авто/новые/подержанные (all/new/used)
            list,  // Список
            sortParam // Параметр для сортировки
        } = this.props;

        const filtredList = /* Нужно получить фильтрованный и сортированный список */

        return(
            <TableColumns
                isMobile={isMobile}
                toggleOpenSection={toggleOpenSection}
                sections={sections}
                list={filtredList}
                tabSign={tabSign}
                removeColumn={removeColumn}
            />
        )
    }
}
const mapStateToProps = (state) => ({
    tabSign: state.tabSign,
    list: state.list,
    sections: state.creatableRows,
    isMobile: state.isMobile,
    sortParam: state.sortParam
});


export default connect(mapStateToProps, null)(TableColumnsContainer);

Ещё раз… У нас есть родительский компонент (hight order component), список (array, содержит объекты), таб, по которому фильтруем(все/новые/c пробегом), параметр, по которому фильтруем. Ну и собственно дочерний компонент, в котором это все рисуется.

Алгоритм

Я уже говорил, что не буду изменять store. Список я буду изменять в нашем родительском компоненте. Как именно:

  1. Создам функцию, которая будет возвращать инстанс объекта (который тоже создам).
  2. Создам новый объект с методами
  3. Реализую методы фильтрации и сортировки
  4. Реализую метод для возврата массива с новым списком
  5. Вызову функцию на массиве из store, и используя методы, сделаю преобразования.

Шаг первый: создать функцию

Функция, которая вернёт мне экземпляр объекта:

export default function _CTFI(array) {
    if(Array.isArray(array)){
        return new _CTF(array);
    } else {
        console.error('variable is not array') // Тут можете поменять как вам удобно
    }
}

Бам! Так просто.

Шаг второй: Новый объект (класс)

Создадим новый объект:

const _CTF = function (vars) {
    if(this instanceof _CTF){
        this.data = JSON.parse(JSON.stringify(vars)); // Обязательно JJSON.parse(JSON.stringify(vars)) или другой способ deepСlone объектов, например _.cloneDeep
        return this;
    } else {
        return new _CTF(vars);
    }
}

Шаг третий: реализовать методы

Фильтрация по табам:

_CTF.prototype.filterByState = function (state) {
    switch(state){
        case 'new':
            this.data = this.data.filter((Car)=> ( Car.CarState === 'Новый' ));
            break;
        case 'used':
            this.data = this.data.filter((Car)=> ( Car.CarState === 'С пробегом' ));
            break;
        default:
            break;
    }
    return this;
}

Здесь я использую стандартные функции JS. В метод будет сообщаться состояние табов (all/new/used) и в зависимости от этого, будет происходить фильтрация через метод filter.

Сортировка по параметрам:

_CTF.prototype.orderByParam = function (param) {
    if(param === null){
        return this;
    } else {
        switch(param){
            case 'CarPrice':
                this.data = this.data.sort(this._sortByPrice);
                return this;
            case 'IsMarkedDown':
                this.data = this.data.sort(this._sortByMarkedDown);
                return this;
            case 'CarRun':
                this.data = this.data.sort(this._sortByCarRun);
                return this;
            default:
                console.warn(`Нет правила для сортировки по параметру ${param}`);
                return this;
        }
    }
}

Плюс несколько функций сортировки:

_CTF.prototype._sortByPrice = function (a, b) {
    const pA = parseInt(a.CarPrice);
    const pB = parseInt(b.CarPrice);
    if(pA < pB){
        return -1;
    } else {
        return 1;
    }
}
_CTF.prototype._sortByMarkedDown = function (a, b) {
    const pA = (a.IsMarkedDown === 1);
    const pB = (b.IsMarkedDown === 1);
    if(pA !== pB){
        if(pA && !pB){
            return -1
        } else {
            return 1
        }
    } else {
        return 0;
    }
}
_CTF.prototype._sortByCarRun = function (a, b) {
    const pA = parseInt(a.CarRun);
    const pB = parseInt(b.CarRun);
    if(pA < pB){
        return -1;
    } else {
        return 1;
    }
}

Шаг четвёртый: Вернуть массив из нашего объекта

Так как мы ранее сохраняли массив в нашем объекте в свойстве data, то у нас нет проблем.

_CTF.prototype.get = function () {
    return this.data;
}

Шаг пятый: Вернёмся в компонент

Мы должны получить наш новый список:

const { tabSign, // какой таб? это параметр для фильтрации. В моём случае это все авто/новые/подержанные (all/new/used)
            list,  // Список
            sortParam // Параметр для сортировки
} = this.props;

        const filtredList = _CTFI(list).filterByState(tabSign).orderByParam(sortParam).get(); 

Получился очень изящно и читабельно.

Можно ли использовать в redux?!

А почему нет?! Только нам нужно будет хранить экземпляр нашего списка без изменений. Мы можем использовать наши методы и для в action’на. Это будет удобно если мы используем наш список в нескольких местах. Например:

export const filterAndSort  = (string) => (
    (dispatch, getState) => {
        const list = getState().list;
		const tabSign = getState().tabSign;
		const sortParam  = getState().sortParam;
		const filtredList = _CTFI(list).filterByState(tabSign).orderByParam(sortParam).get();
		dispatch({ type: SET_FILTRED_LIST, payload: filtredList });
    }
)

Но в таком случае, если у нас происходит изменение списка, нужно изменить и фильтрованный список:

export const changeListByParam  = (param) => (
    (dispatch, getState) => {
        const list = getState().list;
		const tabSign = getState().tabSign;
		const sortParam  = getState().sortParam;
		const newList = fnForChange(list, param);
		const filtredList = _CTFI(newList).filterByState(tabSign).orderByParam(sortParam).get();
		dispatch({ type: SET_LIST, payload: newList });
		dispatch({ type: SET_FILTRED_LIST, payload: filtredList });
    }
)

И ещё кое-что

Мы же можем так делать огромные цепочки фильтрации:

_CTF.prototype.filterByParams= function (param, fnForFilter) {
    if(param === null){
        return this;
    } else {
        switch(param){
            case 'CarPrice':
                this.data = fnForFilter(this.data, param); // Различные функции для фильтрации
                return this;
            case 'IsMarkedDown':
                this.data = fnForFilter(this.data, param);
                return this;
            case 'CarRun':
                this.data = fnForFilter(this.data, param);
                return this;
            default:
                return this;
        }
    }
}

Тогда мы сможем вызывать его так:

const filtredList = _CTFI(list)
.filterByParams('CarPrice', (data, param)=>{
   return data.filter((obj)=>{ obj[param] > 100000 })
})
.filterByParams('IsMarkedDown', (data, param)=>{
   return data.filter((obj)=>{ obj[param] === 1 })
})
/* И так далее */
.get(); // В конце get
// Обновление, новая версия класса

export default function _CTFI(array) {
    if(Array.isArray(array)){
        return new _CTF(array);
    } else {
        console.error('variable is not array') // Тут можете поменять как вам удобно
    }
}

export class _CTF {
    constructor(vars){
        if(this instanceof _CTF){
            this.data = [...vars]
            return this
        } else {
            return new _CTF(vars); 
        }
        
    }

    filterByState = (state) => {
        switch(state){
            case 'new':
                this.data = this.data.filter((Car)=> ( Car.CarState === 'Новый' ));
                break;
            case 'used':
                this.data = this.data.filter((Car)=> ( Car.CarState === 'С пробегом' ));
                break;
            default:
                break;
        }
        return this;
    }

    orderByParam = (param) => {
        if(param === null){
            return this;
        } else {
            switch(param){
                case 'CarPrice':
                    this.data = this.data.sort(this._sortByPrice);
                    return this;
                case 'IsMarkedDown':
                    this.data = this.data.sort(this._sortByMarkedDown);
                    return this;
                case 'CarRun':
                    this.data = this.data.sort(this._sortByCarRun);
                    return this;
                default:
                    console.warn(`Нет правила для сортировки по параметру ${param}`);
                    return this;
            }
        }
    }

    _sortByPrice = (a, b) => {
        const pA = parseInt(a.CarPrice);
        const pB = parseInt(b.CarPrice);
        if(pA < pB){
            return -1;
        } else {
            return 1;
        }
    }

    _sortByMarkedDown = (a, b) => {
        const pA = (a.IsMarkedDown === 1);
        const pB = (b.IsMarkedDown === 1);
        if(pA !== pB){
            if(pA && !pB){
                return -1
            } else {
                return 1
            }
        } else {
            return 0;
        }
    }

    _sortByCarRun = (a, b) => {
        const pA = parseInt(a.CarRun);
        const pB = parseInt(b.CarRun);
        if(pA < pB){
            return -1;
        } else {
            return 1;
        }
    }

    get = () => {
        return this.data;
    }
}