Нормализация данных в Redux flow, мемоизация и Reselect

Нормализация данных в Redux flow

Йо-йо! уже несколько месяцев я работаю frontend-разработчкиком на сайтом автомобильной компании, но из-за изменений в компании я решил найти новую работу. Пока я ходил по собеседованиям узнал несколько новых штук.

Один из вопросов на себеседовании был — «как нормализовывать данные в redux flow для компонентов». Ранее я писал в статье «Метод фильтрации и сортировки массивов для передачи в компонент. React/Redux» про то как я это делал с помощью компонентов-контейнеров (или HOC). Там был описан действенный метод, но не самый лучший. Почему?! Сейчас я вам раскажу и покажу как делать правильно.

Почему?!

Нормализация данных внутри компонента усложняет его логику и делает менее читаемым. Кроме того, получение данных начинает зависеть от компонента, а не от хранилища, что может сыграть с нами злую шутку в будущем. Кроме вышеперечисленного каждый раз когда мы нормализуем наши данные внутри компонента вся наша логика просчитывается заново, а компонент перерендеривается.

Нормализация

В прошлой статье, где я нормализовывал данные внутри компонента был следующий код:

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 = _CTFI(list).filterByState(tabSign).orderByParam(sortParam).get(); 
        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);

Изменение списка у меня зависило от вкладки tabSign и парамертка сортировки sortParam. Сейчас мы хотим избавиться от нормализации внутри компонента. Для этого мы напишем функцию, которая будет возвращать уже отфильтрованный список. Такая функция в redux называется selector (выборщик рус.). Давайте её напишем.

Selectors и нормализация

В redux рекомендуются использовать минимальное состояние хранилища и извлекать из него данные только по мере необходимости. Кроме того redux рекомендует нам относиться к хранилищу как к базе данных и хранить данные в максимально нормлизованным, без вложений и….

…Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists

То есть хранить, например списки хранить не как массив, а как объект с ключом идентификатором. Это явно не удобно для использования в некоторых компонентах, но обеспечивает максимальную производительность. (я, конечно, пока не образец для подражания).

Для того, чтобы извлекать данные для компонентов у нас есть селекторы, которые как раз нормализуют данные. И в моём примере нормализация происходила бы так:

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, 
            list,
        } = this.props;
        return(
            <TableColumns
                isMobile={isMobile}
                toggleOpenSection={toggleOpenSection}
                sections={sections}
                list={list}
                tabSign={tabSign}
                removeColumn={removeColumn}
            />
        )
    }
}

const getFiltredList = (state) => {
    const { list, tabSign, sortParam } = state;
    return _CTFI(list).filterByState(tabSign).orderByParam(sortParam).get();
}

const mapStateToProps = (state) => ({
    tabSign: state.tabSign,
    list: getFiltredList(state),
    sections: state.creatableRows,
    isMobile: state.isMobile,
});
export default connect(mapStateToProps, null)(TableColumnsContainer);

Я написал функцию-селекотор, которая нормализует для меня данные. При этом я больше не передаю ненужные пропсы (sortParam) в компонент. Таким способом документация redux предлагает нам нормализовывать наши данные.

Reselect, Computing Derived Data, мемоизация

И так мемоизация. Когда мы используем выборщики (selectors) данные получаются из функций, которые вычисляются каждый раз как изменяется наш store, не зависимо, используется ли новые данные в компоненте или нет. Чтобы такого не было, нам хотелось бы, чтобы данные где-то сохранялись и просто были переданы в наш mapStateToProps. Это и называется мемоизацией.

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

Для мемоизаци рекомендуется использовать библиотеку reselect. Давайте создадим мемоизированный селектор.

Установка

Всё просто

npm install reselect

Создание

Давайте создадим новый файл с селектором так будет удобнее. У меня это будет «./store/list.selector.js». Внутри него нужно импортировать метод createSelector

import { createSelector } from 'reselect'

Для того, чтобы наши мемоизированные селекторы работали правильно нужно установить зависимости. Зависимостями в reselect являются функции, которые возвращают какие-то данные. В моём случае фильтрованный список зависит от list, tabSign, sortParam. Нужно написать 3 функции, которые как раз будут определять зависимости.

import { createSelector } from 'reselect'

const getList = (state) => (state.list);
const getTabSign = (state) => (state.tabSign);
const getSortParam = (state) => (state.sortParam);

И определим простой, не мемоизированный селестор.

import { createSelector } from 'reselect'

const getList = (state) => (state.list);
const getTabSign = (state) => (state.tabSign);
const getSortParam = (state) => (state.sortParam);

// Простой селектор
const filtredListSimpleSelector = (list, tabSign, sortParam) => (
    _CTFI(list).filterByState(tabSign).orderByParam(sortParam).get();
)

И воспользуемся методом createSelector, в котором мы определим зависимости и функцию, которая будет принимать зависимости и возвращать результат.

import { createSelector } from 'reselect'
import _CTFI from '../../store/utils';

const getList = (state) => (state.list);
const getTabSign = (state) => (state.tabSign);
const getSortParam = (state) => (state.sortParam);

// Простой селектор
const filtredListSimpleSelector = (list, tabSign, sortParam) => (
_CTFI(list).filterByState(tabSign).orderByParam(sortParam).get();
)

const filtredListSelector = createSelector(
   [getList, getTabSign, getSortParam],
   (list, tabSign, sortParam)=> (filtredListSimpleSelector(list, tabSign, sortParam))
)

// getList -> list
// getTabSign -> getTabSign
// sortParam -> getSortParam

export default filtredListSelector

Использование

Давайте вернёмся в компонент, импортируем наш селектор и используем его

import React, { Component } from 'react';
import TableColumns from './TableColumns'
import {connect} from "react-redux";
import filtredListSelector from '../../store/list.selector.js"';


class TableColumnsContainer extends Component{
    render(){
           // Код компонента
        )
    }
}


const mapStateToProps = (state) => ({
    tabSign: state.tabSign,
    list: filtredListSelector(state), // Использование селектора
    sections: state.creatableRows,
    isMobile: state.isMobile,
});
export default connect(mapStateToProps, null)(TableColumnsContainer);

Так вычисление данных внутри селектора будут происходить, только если нужные нам пропсы изменились.

Конец.

P.S.

Я уже использовал селекторы и reselect, например мне нужно было дизейблить свойства фильтров и в моём случае это упростило код компонента и количество перерендеров, а сами селекторы стали просчитываться очень редко. Даже кажется, что фильтр стал быстрее работать.)