А какая архитектура reactjs/redux приложения у вас?

Добрый день!
Пишу клиентскую часть приложения с использованием reactjs и redux.
После нескольких месяцев работы над проектом накопились вопросы по поводу архитектуры проекта, организации редакса.
Есть следующая структура:
├── src
│   ├── assets
│   ├── components
│   │   ├── App.jsx
│   │   ├── blocks
│   │   ├── common
│   │   ├── settings
│   │   │   ├── components
│   │   │   │   ├── accounts
│   │   │   │   ├── buttonDotted
│   │   │   │   ├── collections
│   │   │   │   ├── settingsFilter
│   │   │   │   └── stores
│   │   │   │       ├── components
│   │   │   │       │   └── store
│   │   │   │       ├── stores.css
│   │   │   │       └── stores.jsx
│   │   │   ├── settings.css
│   │   │   ├── settings.jsx
│   │   │   ├── settingsNavigation.jsx
│   │   │   └── settingsRouter.jsx
│   │   └── signup
│   ├── constants
│   ├── index.js
│   ├── store
│   │   ├── actions
│   │   ├── constants
│   │   ├── reducers
│   │   └── store.js
│   └── util

Сразу оговорюсь что структура проекта естественно не вся и названия компонентов изменены в целях безопасности, любые совпадения случайны))
Продолжим по теме:
Внутри папки src имеем скелет:
assets - всякая статика(шрифты, картинки, общий css)...
components - код реакта, компоненты как "умные так и глупые";
components/blocks - умные компоненты, которые подключены к стору редакса и используются в более чем одном месте в приложухе;
components/common - глупые компоненты (кнопка, инпут...);
components/settings, components/signup - страницы приложения;
constants - константы для приложения;
store - все добро для редакса;
util- самописные утилиты;
index.js - точка входа.

Подробнее об src/components.
Страницы (signup например - components/signup/signup.jsx) подключены к стору.
Если нужно разделить вьюху страницы от её логики - рядом c signup.jsx будет лежать signupComponent.jsx, как это сделано:
----signup
├── components
├── signup.css
├── signup.jsx
└── signupComponent.jsx

В случае выше signup.jsx будет подключена к стору редакса и будет иметь какую то логику (валидация формы, локальный стейт страницы, хендлеры на разные события типа вызова модалки восстановления пароля и пр.) и пробрасывать все нужное в signupComponent.jsx, а signupComponent.jsx - это глупый компонент, который собирает стили, компоненты из папки signup/components в одну вьюху(страницу), пропускает пропсы дальше в нужные компоненты типа формы, инпутов и пр.
Папка components внутри страницы?
Да - в каждой папке страницы по необходимости есть папка components в которой будут лежать компоненты специфичные для данной страницы (как умные, так и глупые).
Если посмотреть на структуру папок, то у страницы settings есть settingsRouter.jsx, имеем вложенный роутинг внутри settings. Так же внутри settings/components есть компоненты accounts, collections, stores - дочерние роуты для settings, которые в свою очередь подключены к редаксу.
Сама settings.jsx к стору редакса не подключена - в моем случае это просто враппер для компоновки навигации и вьюх дочерних роутов.
На примере components/settings/components/store можно заметить что там есть еще папка components с компонентами для stores, слава богу последняя этом поддереве папка components.
Такая вложенность папок components/ в каждую страницу имеется по всем страницам и по мере необходимости в папке blocks - "умных компонентах".
Рабочий процесс с этим добром такой:
Если умный компонент нужно переиспользовать еще на одной странице то он отправляется из папки components страницы в которой он использовался в папку src/blocks.
Если есть вьюха которую нужно переиспользовать еще на одной странице - то она отправляется в папку src/common
С одной стороны вроде понятно сразу что куда и к кому относится. И напоминает чем то
фрактальную структуру.
Проект у нас небольшой, около 10-ти страниц. Учитывая специфику проекта добавлять новые фичи в такую структуру будет не больно.
Но что то тревожит, глазу не совсем приятно. Все время кажется что организация папок/проекта не "идеальная".
Тут подходим к первым вопросам, даже к просьбе выразить рекомендации взглянув на проект Вашим "свежим глазом".
1) На сколько по вашему мнению мой подход отличается от той же фрактальной структуры проекта в худшую/лучшую сторону?
2) Субъективно, удобнее ложить вложенные компоненты в папку components или на одном уровне папки страницы как это предлагается в статье о фрактальной структуре?
3) Чем-то моя структура может грозить в будущем?
4) Если вы так же использовали атомарную архитектуру или имеете мнение на этот счет, то поделитесь плюсами/минусами в сравнении выбранного мною подхода и атомарной архитектуры.

Очень интересно услышать мнения и изучить рекомендации бывалых.

Далее на повестке дня редакс любимый.
Есть директория src/store в которой включены все actions, reducers, константы для редакса(типы экшнов) и собственно store.js который редьюсеры собирает и точку входа отдает созданное хранилище:
├── store
│   ├── actions
│   │   ├── signup
│   │   └── store
│   │       └── store.js
│   ├── constants
│   ├── reducers
│   │   ├── index.js
│   │   ├── signup
│   │   │   └── index.js
│   │   └── store
│   │       ├── connectToStore.js
│   │       ├── getStores.js
│   │       ├── store.js
│   │       └── syncStore.js
│   └── store.js

Сначала делал по офф. докам редакса - src/actions, src/reducers, константы экшнов в src/constants. По мере роста приложения стало неудобно прыгать между папками. Объединил все связанное с редаксом в папку src/store вплоть до отдельной папки констант. Стало значительно удобнее. Под каждый тип действий в store/actions и в store/reducers своя папка - тоже удобно, порядочек.
НО! Есть одно жирнющее но - дикое дублирование кода.
Что касается экшнов, хочется что бы красиво, понятно что происходит: код экшна.
Функция которую мы пробрасываем к нужному компоненту что бы запросить список какой нибудь полезной инфы - getStoresAction.
Что бы понимать на каком этапе мы находимся, нам нужно 3 экшна для каждой стадии, это:
getStores, getStoresSuccess, getStoresFailure.
Что в редьюсере: код редьюсера
Тоже удобно, все просто и понятно.
В директории редьюсера лежат файлы с импортами функций которые в редьюсере как раз обрабатывают разные экшны. Как выглядят:
код syncStore и код getStore
Начав использовать такой подход сразу очевидны плюсы - все под контролем и видно что за что отвечает. Знаем точно когда спиннер загрузки покрутить, когда сообщение об ошибке бросить.
Код начал расти, копипаста тоже. Я успокаивал себя мыслью что такой подход дает полный контроль и в случае чего, я смогу без труда вносить изменения в работу той или иной части приложения не затрагивая другие её части. Но по мере роста приложухи я понял что на копипасту и пересоздания одинаковых экшнов и редьюсеров(с разными именами :) ) я трачу много времени - которого жалко.
Я начал гуглить и все варианты что я находил субъективно были достаточно сложными и предполагали в моем случае переписать около половины проекта - увы не вариант.
Встречал подход, в котором все умные компоненты включали в себя свои экшны, редьюсеры, константы и все что ему нужно, условно его можно было портировать в другой проект - модульная архитектура вроде. Что интересно - там главный редьюсер как то по хитромудрому собирает из всех компонентов редьюсеры и компонует их в один редьюсер подставляя названия экшнов из компонента, что то вроде: `${store}.FETCH`, `${store}.FETCH_SUCCESS` и так далее. При этом используя для экшнов с приставкой ".FETCH" один и тот же обработчик, для ".FETCH_SUCCESS" другой. И дублирования кода как у меня - не было. В общем склоняюсь в следующем проекте к такой архитектуре. Но вникать не стал, потому что текущему проекту такой глубокий рефакторинг ни к чему.
Так же у меня были мысли сделать несколько общих функций для редьюсеров и одинаковые места обрабатывать одними и темы же функциями, что уменьшит общее кол-во кода в редьюсерах, но немного уменьшит гибкость.

5) Что вы думаете о модульной структуре? Может есть примеры, опыт которыми можете поделиться?
6) Как вы боретесь с таким дублированием кода в редьюсерах и экшнах как у меня?
7) Исходя из того кода, что я имею сейчас можно ли отделаться малой кровью и избавиться от дублирования кода?
8) Как вы организовываете ваш редакс?

У меня есть экшны которые делают запросы к апи.
Но мне нужно полученные данные изменить. Пришел массив объектов с сервера, а мне в каждый объект нужно добавить пару полей с булевыми значениями(кейс: на одной странице можем выбирать сторы, и на основе выбора будут подгружаться данные, а на другой странице те же сторы мы можем так же выбирать но на основе выбранных сторов будет подгружаться скажем аналитика). Вот и нужно каждому объекту стора добавить два поля: checkedInStoresPage, checkedInAnalyticPage.
9) Вопрос, в каком месте собирать логику которая с данными делает преобразования, в экшне или в редьюсере?
И последнее:
Есть экшны, которые не делают запросы к апи, например из вопроса выше изменения состояния checkedInStoresPage/checkedInAnalyticPage.
Сейчас имею следующий экшн:
код toggleStoreAction
Это работает, это просто и легко.
10) Но можно ли хранить такую логику в экшнах или выносить в редьюсер?
Спасибо вам что уделили время, хорошего дня!
  • Вопрос задан
  • 6293 просмотра
Решения вопроса 2
rockon404
@rockon404 Куратор тега React
Frontend Developer
Есть два хороших подхода к организации кодовой базы, которые подходят для большинства проектов: File Type First и Feature First:

Пример Feature First

Проект:
/common
  /api
  /components
  /ducks
  /entities
  /sagas
  /selectors
  /utils
/features
  /Feature1
  /Feature2
  /Feature3
  /Feature4
  ...
  /FeatureN
/Main
  /pages
  index.js
  App.js
  routes.js
  rootReducer.js
  rootSaga.js
  store.js
/Auth
  /pages
  index.js
  App.js
  routes.js
  rootReducer.js
  rootSaga.js
  store.js
...

Отдельно взятая Feature:
/features
  /Accounts
    /components
    index.js
    accountsDucks.js
    accountsSaga.js
    accountsSelectors.js
    accountsApi.js
    Accounts.js
    AccountsContainer.js

 
Пример File Type First
/actions
/common
/components
  /core
  /Feed
  /Profile
  ...
/constraints
/containers
/entries
/locales
/pages
/reducers
/utils
...


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

Дата мапперы в зависимости от задач можно использовать в редьюсерах, в mapStateToProps и asyncActions. Главное чтобы по проекту все было стандартизировано.
В mapStateToProps пишут преобразования необходимые лишь для одного компонента.

Большое количество бойлерплейта это плата которую вы платите за использование redux. Можно писать все константы и редьюсеры руками, можно использовать библиотеки вроде redux-actions и ей подобные. В первом случае вы получаете плюс к гибкости, читаемости и статическому анализу, во втором меньше кода. Я в большинстве проектов предпочитаю первый вариант. Так же создаю шаблоны файлов в Webstorm для asyncActions, contstraints, редьюсера, страницы, компонента и законнекченого компонента.
В специфичных проектах с множеством CRUD запросов и похожих сущностей есть смысл написать CRUD Boilerpalte.
Ответ написан
Комментировать
SaymonA
@SaymonA Автор вопроса
Нет универсального подхода.
Структурировать проект нужно так как удобно лично Вам.

Но что бы понять как это "удобно" и при этом не запороть проект я придерживаюсь некоторых идей, описанных в статьях:
Первая
Вторая

В итоге получается что то похожее на ответ nakree.
Несколько месяцев ушло что бы прийти к такой архитектуре.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 2
nakree
@nakree
Fullstack Developer
src/
--utils/
--config/
--css/
--img/
--components/
----User/
------User.jsx
------UserContainer.js
------UserReducer.js
------UserActions.js
Ответ написан
Отказался от деления компонентов на умные и глупые. Любому компоненту может потребоваться прямой доступ к данным, и перекидывать лапшу в props - неблагодарное занятие. У меня в целом все просто: компоненты в папке Components, редьюсеры в папке Reducers, экшены в Actions, константы в Constants, и все остальное по назначению.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы