Redux store: Расширение по «горизонтали»

  • Tutorial
Redux Когда приложение, использующее Redux, разрастается до достаточно больших размеров, количество состояний увеличивается многократно. Для разделения редьюсеров на логические единицы применяется подход комбинирования их с помощью combineReducers. Данное решение позволяет расширить store по «вертикали». Но бывают случаи, когда данного разделения может быть недостаточно. Например, один из уровней несет в себе составную логику, которую тоже было бы неплохо разделить (или как говорил один из известных людей: «Ухлубить!»). Но такого подхода нет в API Redux. И поиск решения данного вопроса так же ничего не дал (может плохо искал). Поэтому я разработал свой подход расширения по «горизонтали» Redux Store.

Хочу Вас ознакомить со своим проектом, который позволяет осуществить данный подход.

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


1) Сначала, на уровне редьюсера первого уровня, подключаем саму библиотеку:

import {stateCombine, runCombine, getInitialState} from "redux-combine-deep-props";

2) Подключаем редьюсер для второго уровня:

import level2Module from "./reducer-level-2";

3) Формируем начальные значения для первого уровня:

let initialState = {
propLevel1: ...,
...
propLevelN: ...
};

4) Создаем объект комбинаций:

let combinations = {
	<name prop>: {
		module: level2Module
	}
};

где мы задаем название будущего раздела name prop, и для него — редьюсер этого уровня, а так же набор типов эшенов для триггеринга изменения стейта данного уровня.

5) Создаем функцию-обработчик текущего стейта:

let combineDeepProp = stateCombine(combinations);
let combine = runCombine(combinations, combineDeepProp);

6) Для обработки начальных значений всех уровней создаем комбинированный initial state.

const combineInitialState = getInitialState(combinations, initialState);

7) В экспортной функции-редьюсере используем комбинированный initial state, а в ее теле строго до любого изменения состояния запускаем обработчик всех комбинаций, который меняет нужным образом текущее состояние, если текущий тип экшена совпал с заданными:

export default function level1Module (state = combineInitialState, action) {
  ...
  let newState = combine(state, action);
  ...
    switch (action.type) {
      case "....":
        newState = {
           ...newState,
           ...
        };

        break;
      ...
    };
  ...
  return newState;
};

8) Модуль второго уровня оформляется по стандартной схеме, с учетом, что стейт в нем представлен в разрезе этого уровня:

let initialState = {
...
};

export default function search(state = initialState, action) {
   ...
   switch (action && action.type) {
     ...
   };
};

но с одним отличием — должна быть проверка на undefined текущего action. Сделано для задание initial state при первом проходе в методе getInitialState.

Заключение


Данный подход позволяет в рекурсивном режиме расширить до бесконечности текущий уровень и по «вертикали», за счет использованием в комбинациях более одного объекта:

let combinations = {
	<name prop1>: {
		module: level2Module1
	},
        ...
        <name propN>: {
		module: level2ModuleN
	}
};

и по «горизонтали», за счет использования описанного выше подхода на каждом из 2+ уровней.

Исходники

UPD:
Полный рефакторинг кода, большое спасибо dagen, за указание на проблему мутабельности. Теперь немного поменялся принцип использования, смотрите п.7 и п.4 — набор экшенов теперь отсутствует за ненадобностью, но комбинации пока оставил как объект для возможного дальнейшего расширения функционала. Замечу, что данный подход я использовал со связкой с PolymerJS, а потом с VueJS, и использовал для интеграции с Redux библиотеки polymer-redux и vuedeux соответственно. И так как там бинд на конкретные свойства стейта идут по пути, то меня и миновала проблема мутаций, так как было необязательно мутировоать рутовый стейт при мутировании одного из поддеревьев.
UPD2:
Добавил сборщик rollup для компиляции проекта
Может ли быть полезным данный подход?

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

Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 15
  • +1

    Расскажите, чем ваш подход лучше использования вложенных combineReducers (https://github.com/reactjs/redux/issues/738) вместе с reduce-reducers от Эндрю Кларка?

    • –1
      Как я понял — это теоретическое рассуждение на тему. Без практической реализации. Я предоставил практический подход к решению данной задачи.
      • +1
        Да нет, вот вам практическая реализация(из issue по ссылке) которая вообщем-то должна работать прекрасно:

        rootReducer = combineReducers({
          router, // redux-react-router reducer
            account: combineReducers({
              profile: combineReducers({
                 info, // reducer function
                 credentials // reducer function
              }),
              billing // reducer function
            }),
            // ... other combineReducers
          })
        });
        
        • –1
          В такой архитектуре все пропертис — это обязательно должна быть функция-редьюсер.
          В моем подходе в раздел «profile», например, проперти «isUse». И я бы не смог этого сделать — пришлось бы запихивать его в один из разделов «info» или «credentials», или вообще создавать новый. Все это привело бы к ненужной избыточности иерархии. Мой подход, считаю, более гибкий в этом отношении.
          • 0
            Вы хотите вот такую структуру в ветке стейта?
            profile: {
              info: Object,
              credentials: Object,
              isUsed: boolean,
            }       
            

            Хозяин — барин. Обсуждение, зачем именно это понадобилось — непродуктивно. Но если я правильно вас понял, то вот простой пример решения:
            const isProfileUsed = state => ({ ...state, isUsed: true });
            const profileParams = combineReducers({
              info,
              credentials,
            });
            
            const rootReducer = combineReducers({
              router,
              account: combineReducers({
                profile: reduceReducers(profileParams, isProfileUsed)
                billing,
              }),
              // ... other combineReducers
            });
            

            Конечно же isUsed — это просто чистая функция, как и любой редьюсер, и вы можете делать с ней всё, что захотите. Например в привычном виде (у вас в примерах это switch, у меня — createReducer) сделать обработку по нужным actionTypes, а вместо spread-оператора использовать Ramda, Immutable и так далее. Не придумывая дополнительные соглашения по нотации «объекта комбинаций».
            • 0
              Это еще один подход к решению подобной задачи, но вы здесь используете reduce-reducers, то есть не решаете задачу родным API. Поэтому отличие только в реализации. На панацею не претендую, но думаю мой подход кому-нибудь может быть полезен.
              • 0
                Конечно, может быть полезен :) Просто мы обсуждали решение проблемы более «проторенным» путём. Как видите, такой путь есть. Возможно поэтому в опросе подавляющее большинство голосов «нет» и «скорее всего нет»?

                Насчёт родного API: redux не даёт такого апи вообще. Всё, что есть — это набор соглашений и полная свобода делать что угодно с корневым редьюсером (кто во что горазд — как было с классическим Flux до появления Redux, который стал стандартом де-факто). CombineReducers был введён лишь чтобы дать похожую на классический Flux возможность разделения общего стейта на отдельные «домены». И в работе с составной логикой в редьюсерах тоже есть стандарт де-факто: обычный функциональный подход.

                Btw, у вас могут быть проблемы с мутацией стейта.
                • 0
                  Большое спасибо за ревью кода, на самом деле такие проблемы имеют место. В понедельник сделаю фикс.
              • 0
                dagen расскажите, пожалуйста, в чем смысл такой конструкции?
                Зачем комбинировать info и credentials в profileParams, если потом reduceReducers снова его раскомбинирует в плоскую структуру? Почему просто не указать info, credentials на уровне account-reducer?
                • 0
                  Почему вы решили, что reduceReducers «раскомбинирует» в плоскую структуру? Это же обычный функциональный fold всех редьюсеров. В моём примере никакого раскомбинирования в плоскую структуру не происходит, это просто slice reducer, который отвечает не за ветки info и credentials, а за весь их родительский узел profile.

                  А зачем так делать — спросите у автора. Я лишь предложил более простой (по моему скромному мнению) вариант решения проблемы, который проще, чем предложенный автором. Тоже считаю, что где-то тут у автора неправильно сформирована структура данных.
        • +1
          Например, один из уровней несет в себе составную логику, которую тоже было бы неплохо разделить (или как говорил один из известных людей: «Ухлубить!»). Но такого подхода нет в API Redux.

          Углубление ни к чему хорошему не приведет. Во-первых вложенное дерево намного сложнее поддерживать, усложняется логика редьюсеров, во-вторых усложняется логика селекторов, что может вылезти неприятными сюрпризами при рефакторинге, в-третьих это может повлиять на производительность (у меня в одном проекте даже redux-devtools через раз открывался).

          Изначально, при знакомстве с редаксом, у меня возникла идея обернуть в combineReducers все ветки, чтобы можно было передавать только схему стора, но попробовав на практике решил использовать наиболее плоскую структуру.
          • –1
            А Вы пробовали на практике эту схему с приложенным кодом? combineReducers — не для этого.
            Вложенные структуры оправдывают себя при разведении логики по отдельным файлам, а не писать все редьюсеры кучей в руте. И данный могут хранится в нормализованном виде. Данные никакого отношения не несут на структуру обращения к ним. Я использовал redux со связкой с polymerjs, а сейчас с vue и там я биндю стор через путь, что не несет за собой никакой нагрузки. А вот когда происходит оперирование со сложной иерархической структурой в редьюсере, согласен, что при большом размере структуры могут быть задержки выполнения, но это на себя берет lodash. И это уже дела оптимизации, необязательно же уходить сильно далеко вглубь, и не обязательно хранить большие объемы данных, можно периодически скидывать кэш(в редаксе есть для этого расширения). Но в целом задача по разделению логики решена, причем в достаточно автоматическом режиме: в каждом редьюсере работаете как в руте, и один только раз описывайте комбинации, остальное дело техники.
            • 0
              Документация Redux как раз и предлагает не писать все редьюсеры кучей в руте. Более того, прямым тестом сказано, что combineReducers — просто пример, что каждый может сам себе режиссёр реализовать свой вариант combineReducers. Так что формально ваше предложение не нарушает заветы авторов Redux.

              Но для декомпозиции редьюсеров обычно используют функциональный подход, и это оказывается намного удобнее. А для работы с вложенным деревом — нормализацию стейта.
          • 0
            Я ведь не спорю что есть способы добиться того же, например. Но я преследовал цели гибкости и автоматизма и нормализации логики по слоям(разделам) стора.
            • 0
              Добавил сборщик для компиляции проекта

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.