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 для компиляции проекта

Only registered users can participate in poll. Log in, please.

Может ли быть полезным данный подход?

Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 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
              Добавил сборщик для компиляции проекта

              Only users with full accounts can post comments. Log in, please.