https://habrahabr.ru/post/327196/- Разработка веб-сайтов
 - Проектирование и рефакторинг
 - Анализ и проектирование систем
 - JavaScript
 
В первой части я уделил внимание только общей концепции: редюсеры, компоненты и экшны чаще меняются одновременно, а не по отдельности, поэтому и группировать и их целесообразнее по модулям, а не по отдельным папкам 
actions, 
components, 
reducers. Также к модулям были предъявлены требования:
- быть независимыми друг от друга
 
- взаимодействовать с приложением через API ядра
 
В этой части я расскажу о структуре ядра, подходящей для разработки data-driven систем.
Начнем с определения модуля. Работать с простым объектом не совсем удобно. Добавим немного ООП:
const _base = Symbol('base')
const _ref = Symbol('ref')
class ModuleBase{
  constructor(base){
    this[_base] = base
    this[_ref] = getRef(this)
  }
  /**
   * unique module id
   * @returns {string}
   */
  get id(){
    return this.constructor.name
  }
  /**
   * full module ref including all parents
   * @returns {string}
   */
  get ref(){
    return this[_ref]
  }
  /**
   * module title in navigation
   * @returns {string}
   */
  get title(){
    return this.id
  }
  /**
   * module group in navigation
   * @returns {string}
   */
  get group(){
    return null
  }
  /**
   * react component
   * @returns {function}
   */
  get component() {
    return null
  }
  /**
   * router route
   * @return {object}
   */
  get route(){
    return getRoute(this)
  }
  /**
   * router path
   * @return {string}
   */
  get path(){
    return this.id
  }
  /**
   * children modules
   * @return {Array}
   */
  get children(){
    return []
  }
  /**
   * @type {function}
   */
  reduce
  //....
}
В коде выше для реализации инкапсуляции используются символы.
Теперь объявление модуля более привычно – необходимо унаследовать класс 
ModuleBase, переопределить необходимые геттеры и по желанию добавить функцию 
reduce, которая будет выполняться функцию редюсера.
В прошлый раз мы ограничили вложенность модулей вторым уровнем. В реальных приложениях этого бывает недостаточно. Кроме этого в прошлый раз нам нужно было выбирать между редюсером родительского модуля и комбинацией редюсеров дочерних. Это «ломает» композицию.
Например, если мы хотим создать стандартный CRUD над сущностью в БД логично организовать модули так:
/SomeEntity
  /components
    /Master.js
  /children
    /index.js
    /create.js
    /update.js
  /index.js
Считаем, что для 
create и 
update используются стандартный компонент формы, а для вывода данных стандартный компонент 
Grid из ядра системы, поэтому достаточно определить только модули для этих операций.
Родительский модуль отвечает за вывод лейаута, ссылок «создать», «назад к списку» и сообщений об успешности или не успешности запросов к серверу. 
Index – за фильтрацию, пагинацию и ссылки. 
Create и 
Update выводят формы на создание и редактирование. 
Таким образом, редюсер родительского модуля должен иметь доступ ко всему подграфу состояния модуля, а дочерние – каждый к своей части. Реализуем две функции компоновки.
Для роутов
const getRoute = module => {
  const route = {
    path: module.path,
    title: module.title,
    component: module.component
  }
  const children = module.children
  if(children) {
    ModuleBase.check(children)
    const index = children.filter(x => x.id.endsWith(INDEX))
    if(index.length > 0){
      // share title with parent module
      route.indexRoute = {
        component: index[0].component
      }
    }
    route.childRoutes = module.children
      .filter(x => !x.id.endsWith(INDEX))
      .map(getRoute)
  }
  return route
}
И для реюсеров
 class ModuleBase{
  //....
  combineReducers(){
    const childrenMap = {}
    let children = Array.isArray(this.children) ? this.children : []
    ModuleBase.check(children)
    const withReducers = children.filter(x => typeof(x.reduce) === 'function' || x.children.length > 0)
    for (let i = 0; i < withReducers.length; i++) {
      childrenMap[children[i].id] = children[i]
    }
    if(withReducers.length == 0){
      return reducerOrDefault(this.reduce)
    }
    const reducers = {}
    for(let i in childrenMap){
      reducers[i] = childrenMap[i].combineReducers()
    }
    const parent = this
    const reducer = typeof(this.reduce) === 'function'
      ? (state, action) => {
        if(!state){
          state = parent.initialState
        }
        const nextState = parent.reduce(state, action)
        if(typeof(nextState) !== 'object'){
          throw Error(parent.id + '.reduce returned wrong value. Reducers must return plain objects')
        }
        for(let i in childrenMap){
          if(!nextState[i]){
            nextState[i] = childrenMap[i].initialState
          }
          nextState[i] = {...reducers[i](nextState[i], action)}
          if(typeof(nextState[i]) !== 'object'){
            throw Error(childrenMap[i].id + '.reduce returned wrong value. Reducers must return plain objects')
          }
        }
        return {...nextState}
      }
      : combineReducers(reducers)
    return reducer
  }
Это не самая эффективная реализация подобного редюсера. К сожалению, даже она заняла у меня достаточно много времени. Буду благодарен, если кто-то в комментариях подскажет, как можно сделать лучше.
Соответствие роутов и стейта
Данная реализация модульной системы полагается на соответствие стейта и роутов один к одному, с небольшими исключениями:
/Update заменяется на /:id 
/Index опускается (используется indexRoute) 
- Для 
Delete нет своего роута. Удаление производится из модуля Index 
Метод path можно переопределить и тогда роут будет отличать от названия модуля. Можно конструировать цепочки модулей любой вложенности. Более того, если в вашем приложении только один корневой роут 
/, то целесообразно сделать модуль 
App и вложить в него все остальные, чтобы использовать один подход повсеместно.
Это позволит в редюсере App (если такой нужен) обрабатывать любые события приложения и модифицировать состояние любого дочернего модуля. Пожалуй, это слишком круто для любого, даже самого крутого редюсера. Я не рекомендую вообще переопределять reduce для родительского модуля приложения. Однако, такой редюсер может быть полезен для каких-то системных операций.
С роутингом покончено, осталось «
законектить» компоненты к стейту. Так как редюсеры скомпонованы рекурсивно в соответствие со вложенностью дочерних модулей коннектить будем также. Здесь все просто. Реализацию 
mapDispatchToProps рассмотрим чуть ниже.
Компоненты ядра
Итак, 
ModuleBase– первая и неотъемлемая часть ядра. Без него свой код к приложению вообще не подцепить. 
ModuleBase предоставляет следующее API:
- Регистрация компонента в роутере
 
- Регистрация редюсера модуля
 
- Connect компонентов к стейту redux
 
Не плохо, но недостаточно. 
CRUD должно быть делать просто. Добавим 
DataGridModuleBase и 
FormModuleBase. До текущего момента мы не уточняли какие компоненты используются в модулях. 
Компоненты и контейнеры
Контейнеры – один из широко распространённых паттернов в React. Если коротко, то разница между компонентами и контейнерами в следующем:
- Компоненты (или презентационные компоненты) не содержат внешних зависимостей и логики
 
- Контейнеры (как понятно из названия) оборачивают компоненты, реализуя байндинг между внешним миром и компонентами
 
Контейнеры (как понятно из названия) оборачивают компоненты, реализуя байндинг между внешним миром и слоем представления.
Такая организация улучшает повторное использование кода, упрощает разделение работы между разными специалистами и тестирование. Функция 
connect по сути является фабрикой контейнеров.
Для разработки 
DataGridModule нам потребуются:
- компонент 
DataGrid 
- его контейнер 
DataGridContainer 
- редюсер для связи между контейнером и состоянием приложения в redux
 
Реализацию презентационного компонента я опускаю. Для подключения к стейту у нас есть функция 
ModuleBase.connect. Осталось получать данные с сервера. Можно на каждый грид создавать новый класс и переопределять 
componentDidMount  или другие методы жизненного цикла компонента. Подход, в целом, рабочий, но имеющий два значительных недостатка:
- гигантское количество boilerplate и копипасты. А копи-пейст, как известно, всегда приводит к ошибкам
 
- низкая скорость разработки модулей (ядро пока не предоставляет никакого API для ускорения разработки, это неправильно)
 
Примеси (mixin)
Расширим возможности компоновки компонентов и контейнеров с помощью mixin’ов. 
class и 
extends – это объекты первого класса в ES6. Иными словами, запись 
const Enhanced = superclass => class extends superclass корректна. Это возможно, благодаря системе наследования JavaScript, основанной на прототипах.
Добавим в ядро функцию 
mix и примеси 
Preloader и 
ServerData:
const Preloader = Component => class extends Component {
  render() {
    const propsToCheck = subset(this.props, this.constructor.initialState)
    let isInitialized = true
    let isFetching = false
    for(let i in propsToCheck){
        if(typeof(propsToCheck[i][IS_FETCHING]) === 'boolean'){
          if(!isFetching && propsToCheck[i][IS_FETCHING]){
            isFetching = true
          }
          // if something except "isFetching" presents it's initialized
          if(isInitialized && Object.keys(propsToCheck[i]).length === 1){
            isInitialized = false
          }
        }
    }
    return isInitialized
      ? (<Dimmer.Dimmable dimmed={isFetching}>
        <Dimmer active={isFetching} inverted>
          <Loader />
        </Dimmer>
        {super.render()}
      </Dimmer.Dimmable>)
      : (<Dimmer.Dimmable dimmed={true}>
        <Dimmer active={true} inverted>
          <Loader />
        </Dimmer>
        <div style={divStyle}></div>
      </Dimmer.Dimmable>)
  }
}
const ServerData = superclass => class extends mix(superclass).with(Preloader) {
  componentDidMount() {
    this.props.queryFor(
      this.props.params,
      subset(this.props, this.constructor.initialState))
  }
Первый проверяет все ключи в стейте и если находит хотя-бы один с определенным свойством 
isFetching: true выводит поверх компонента диммер. Если кроме 
isFetching в объекте свойств нет, считаем, что они должны прийти с сервера и вообще не отображаем компонент (считаем не инициализированным).
Миксин 
ServerData автоматически подмешивает прелоадер и переопределяет 
componentDidMount.
queryFor
Рассмотрим более подробно реализацию queryFor. Ее передал 
Module.connect через 
mapDispatchToProps.
export const queryFactory = dispatch => {
  if(typeof (dispatch) != 'function'){
    throw new Error('dispatch is not a function')
  }
  return (moduleId, url, params = undefined) => {
    dispatch({
      type: combinePath(moduleId, GET),
      params
    })
    return new Promise(resolve => {
      dispatch(function () {
        get(url, params).then(response => {
          const error = 'ok' in response && !response.ok
          const data = error
            ? {ok: response.ok, status: response.status}
            : response
          dispatch({
            type: combinePath(moduleId, GET + (error ? FAILED : SUCCEEDED)),
            ...data
          })
          resolve(data)
        })
      })
    })
  }
}
export const queryAll = (dispatch, moduleRef, params, ...keys) => {
  const query = queryFactory(dispatch)
  if(!keys.length){
    throw new Error('keys array must be not empty')
  }
  const action = combinePath(moduleRef, keys[0])
  let promise = query(action, fixPath(action), params)
  for(let i = 1; i < keys.length; i++){
    promise.then(() => {
      let act = combinePath(moduleRef, keys[i])
      query(act, fixPath(act), params)
    })
  }
}
export const queryFor = (dispatch, moduleRef, params, state) => {
  const keys = []
  for (let i in state) {
    if (state[i].isFetching !== undefined) {
      keys.push(toUpperCamelCase(i))
    }
  }
  return queryAll(dispatch, moduleRef, params, ...keys)
С помощью 
queryFactory создаем функцию 
query, которая делает запрос на сервер, диспатчит в 
store соответствующие события и возвращает 
promise, чтобы можно было выстроить цепочку запросов функции в 
queryAll, список запросов в которую передаст та самая функция 
queryFor, которая ориентируется на наличие 
isFetching в объекте 
в доме, который построил Джек.
Допишем «обогощалку» для стейта, требующего серверных данных:
ServerData.fromServer = (initialState, ...keys) => {
  for(let i = 0; i < keys.length; i++){
    initialState[keys[i]].isFetching = false
  }
  return initialState
}
Теперь достаточно знать правила использования миксина, чтобы сделать из любого компонента, работающего с клиентскими данными на серверный. Достаточно правильно настроить initialState и подключить mixin.
Осталось обработать события старта получения данных, успешного получения и ошибок и изменять соответствующим образом состояние контейнера. Для этого допишем редюсер в модуле.
ServerData.reducerFor
ServerData.reducerFor = (moduleRef, initialState, next = null, method = GET) => {
  if(!moduleRef){
    throw Error('You must provide valid module name')
  }
  if(!initialState){
    throw Error('You must provide valid initialState')
  }
  const reducer = {}
  for (let i in initialState) {
    reducer[i] = hasFetching(initialState, i)
      ? ServerData.serverRequestReducerFactory(combinePath(moduleRef, i), initialState[i], next, method)
      : passThrough(initialState[i])
  }
  if(Object.keys(reducer) < 1){
    throw Error('No "isFetching" found. Cannot build reducer')
  }
  const combined = combineReducers(reducer)
  return combined
}
export default class DataGridModuleBase extends ModuleBase {
  constructor(base){
    super(base)
    // Create is required due to children module
    this.reduce = ServerData.reducerFor(this.ref, DataGridContainer.initialState)
  }
  get component () {
    return this.connect(DataGridContainer)
  }
}
Добавляем модуль с гридом в приложение
export default class SomeEntityGrid extends DataGridModuleBase {
}
//..
const _children= Symbol('children')
export default class App extends ModuleBase{
  constructor(base){
    super(base)
    this[_children] = [new SomeEntityGrid(this)]
  }
  get path (){
    return '/'
  }
  get component () {
    return AppComponent
  }
  get children(){
    return this[_children]
  }
Если вы дочитали до конца, то FromModuleBase сможете реализовать по аналогии.
Финальная структура ядра
/core
  /ModuleBase.js
  /api.js
  /components
  /containers
  /modules
  /mixins
- Базовые модули содержат повторно-используемую логику и наборы стандартных компонентов, часто используемых вместе (например, 
CRUD). 
- Папки 
components и containers содержат часто-используемые компоненты и контейнеры, соответственно. 
- С помощью примесей можно компоновать компоненты и контейнеры: грид с серверными данными, грид с инлайн-вводом, грид с серверными данными и инлайн-вводом и т.д.
 
- api.js содержит функции для работы с сервером: fetch, get, post, put, del,…