Нормализация данных в 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, например мне нужно было дизейблить свойства фильтров и в моём случае это упростило код компонента и количество перерендеров, а сами селекторы стали просчитываться очень редко. Даже кажется, что фильтр стал быстрее работать.)