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

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

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

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

Почему?!

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

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

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

import { useSelector } from 'react-redux'
import TableColumns from './TableColumns'
import _CTFI from '../../store/utils';

export const TableColumnsContainer = (props) => {
    const {
        toggleOpenSection,
        removeColumn
    } = props

    /**
     * Какой таб? это параметр для фильтрации. 
     * В моём случае это все авто/новые/подержанные (all/new/used)
     */
    const tabSign = useSelector(state =>  state.tabSign)
    const list = useSelector(state => state.list) // Список
    const sections = useSelector(state => state.creatableRows)
    const isMobile = useSelector(state => state.isMobile)
    const sortParam = useSelector(state => state.sortParam) // Параметр для сортировки
    

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


    return(
        <TableColumns
            isMobile={isMobile}
            toggleOpenSection={toggleOpenSection}
            sections={sections}
            list={filtredList}
            tabSign={tabSign}
            removeColumn={removeColumn}
        />
    )
}

Изменение списка у меня зависело от вкладки 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 { useSelector } from 'react-redux'
import TableColumns from './TableColumns'
import _CTFI from '../../store/utils';

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

export const TableColumnsContainer = (props) => {
    const {
        toggleOpenSection,
        removeColumn
    } = props

    const tabSign = useSelector(state =>  state.tabSign)
    const list = useSelector(getFiltredList) // Функция для получения
    const sections = useSelector(state => state.creatableRows)
    const isMobile = useSelector(state => state.isMobile)
    
    return(
        <TableColumns
            isMobile={isMobile}
            toggleOpenSection={toggleOpenSection}
            sections={sections}
            list={list}
            tabSign={tabSign}
            removeColumn={removeColumn}
        />
    )
}

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

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

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

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

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

Установка

Всё просто

npm install reselect

А ещё можно использовать для этого @reduxjs/toolkit

Создание

Давайте создадим новый файл с селектором, так будет удобнее. У меня это будет «./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 { useSelector } from 'react-redux'
import TableColumns from './TableColumns'
import filtredListSelector from '../../store/list.selector.js"';


export const TableColumnsContainer = (props) => {
    const {
        toggleOpenSection,
        removeColumn
    } = props

    const tabSign = useSelector(state =>  state.tabSign)
    const list = useSelector(filtredListSelector) // Заменил на селектор
    const sections = useSelector(state => state.creatableRows)
    const isMobile = useSelector(state => state.isMobile)
    
    return(
        <TableColumns
            isMobile={isMobile}
            toggleOpenSection={toggleOpenSection}
            sections={sections}
            list={list}
            tabSign={tabSign}
            removeColumn={removeColumn}
        />
    )
}

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

Конец.

P.S.

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