Описание действий и редьюсеров одна из таких особенностей. Классическая реализация двух этих сущностей в коде довольно трудоемкое занятие.
Боль классической реализации
Простой пример:
// actionTypes.js
// описываем типы действий
export const POPUP_OPEN_START = 'POPUP_OPEN_START ';
export const POPUP_OPEN_PENDING = 'POPUP_OPEN_PENDING ';
export const POPUP_OPEN_SUCCESS = 'POPUP_OPEN_SUCCESS ';
export const POPUP_OPEN_FAIL = 'POPUP_OPEN_FAIL';
export const POPUP_CLOSE_START = 'POPUP_CLOSE_START ';
export const POPUP_CLOSE_PENDING = 'POPUP_CLOSE_PENDING ';
export const POPUP_CLOSE_SUCCESS = 'POPUP_CLOSE_SUCCESS ';
export const POPUP_CLOSE_FAIL = 'POPUP_CLOSE_FAIL';
// actions.js
// описываем сами действия
import {
  POPUP_OPEN_START,
  POPUP_OPEN_PENDING,
  POPUP_OPEN_SUCCESS,
  POPUP_OPEN_FAIL,
  POPUP_CLOSE_START,
  POPUP_CLOSE_PENDING,
  POPUP_CLOSE_SUCCESS,
  POPUP_CLOSE_FAIL
} from './actionTypes';
export function popupOpenStart(name) {
  return {
    type: POPUP_OPEN_START,
    payload: {
      name
    },
  }
}
export function popupOpenPending(name) {
  return {
    type: POPUP_OPEN_PENDING,
    payload: {
      name
    },
  }
}
export function popupOpenFail(error) {
  return {
    type: POPUP_OPEN_FAIL,
    payload: {
      error,
    },
  }
}
export function popupOpenSuccess(name, data) {
  return {
    type: POPUP_OPEN_SUCCESS,
    payload: {
      name,
      data
    },
  }
}
export function popupCloseStart(name) {
  return {
    type: POPUP_CLOSE_START,
    payload: {
      name
    },
  }
}
export function popupClosePending(name) {
  return {
    type: POPUP_CLOSE_PENDING,
    payload: {
      name
    },
  }
}
export function popupCloseFail(error) {
  return {
    type: POPUP_CLOSE_FAIL,
    payload: {
      error,
    },
  }
}
export function popupCloseSuccess(name) {
  return {
    type: POPUP_CLOSE_SUCCESS,
    payload: {
      name
    },
  }
}
// reducers.js
// реализуем редьюсеры
import {
  POPUP_OPEN_START,
  POPUP_OPEN_PENDING,
  POPUP_OPEN_SUCCESS,
  POPUP_OPEN_FAIL,
  POPUP_CLOSE_START,
  POPUP_CLOSE_PENDING,
  POPUP_CLOSE_SUCCESS,
  POPUP_CLOSE_FAIL
} from './actionTypes';
const initialState = {
  opened: []
};
export function popupReducer(state = initialState, action) {
  switch (action.type) {
    case POPUP_OPEN_START:
    case POPUP_OPEN_PENDING:
    case POPUP_CLOSE_START:
    case POPUP_CLOSE_PENDING:
      return {
        ...state,
        error: null,
        loading: true
      };
    case POPUP_OPEN_SUCCESS :
       return {
        ...state,
        loading: false,
        opened: [
          ...(state.opened || []).filter(x => x.name !== action.payload.name),
          {
             ...action.payload
          }
        ]
      };
    case POPUP_OPEN_FAIL:
      return {
        ...state,
        loading: false,
        error: action.payload.error
      };
    
    case POPUP_CLOSE_SUCCESS:
       return {
        ...state,
        loading: false,
        opened: [
            ...state.opened.filter(x => x.name !== name)
        ]
      };
    case POPUP_CLOSE_FAIL:
      return {
        ...state,
        loading: false,
        error: action.payload.error
      };
  }
  return state;
}
На выходе имеем 3 файла и как минимум следующие проблемы:
- «раздувание» кода при простом добавлении новой цепочки действий
- избыточный импорт констант действий
- чтение имен констант действий (индивидуально)
Оптимизация
Данный пример можно улучшить с помощью redux-actions.
import { createActions, handleActions, combineActions } from 'redux-actions'
export const actions = createActions({
    popups: {
        open: {
            start: () => ({ loading: true }),
            pending: () => ({ loading: true }),
            fail: (error) => ({ loading: false, error }),
            success: (name, data) => ({ loading: false, name, data }),
        },
        close: {
            start: () => ({ loading: true }),
            pending: () => ({ loading: true }),
            fail: (error) => ({ loading: false, error }),
            success: (name) => ({ loading: false, name }),
        },
    },
}).popups
const initialState = {
    opened: []
};
export const accountsReducer = handleActions({
    [
        combineActions(
            actions.open.start,
            actions.open.pending,
            actions.open.success,
            actions.open.fail,
            actions.close.start,
            actions.close.pending,
            actions.close.success,
            actions.close.fail
        )
    ]: (state, { payload: { loading } }) => ({ ...state, loading }),
    [combineActions(actions.open.fail, actions.close.fail)]: (state, { payload: { error } }) => ({ ...state, error }),
    [actions.open.success]: (state, { payload: { name, data } }) => ({
        ...state,
        error: null,
        opened:
        [
            ...(state.opened || []).filter(x => x.name !== name),
            {
                name, data
            }
        ]
    }),
    [actions.close.success]: (state, { payload: { name } }) => ({
        ...state,
        error: null,
        opened:
        [
            ...state.opened.filter(x => x.name !== name)
        ]
    })
}, initialState)
Уже намного лучше, но совершенству нет предела.
Лечим боль
В поисках более оптимального решения наткнулись на комментарий LestaD habr.com/ru/post/350850/#comment_10706454 и решили попробовать redux-symbiote.
Это позволило убрать лишние сущности и уменьшить количество кода.
Пример выше стал выглядеть вот так:
// symbiotes/popups.js
import { createSymbiote } from 'redux-symbiote';
export const initState = {
  opened: []
};
export const { actions, reducer } = createSymbiote(initialState, {
  popups: {
    open: {
      start: state => ({ ...state, error: null }),
      pending: state => ({ ...state }),
      success: (state, { name, data } = {}) => ({
        ...state,
        opened: [
            ...(state.opened || []).filter(x => x.name !== name),
            {
              name,
              data
            })
        ]
      }),
      fail: (state, { error } = {}) => ({ ...state, error })
    },
    close: {
      start: state => ({ ...state, error: null }),
      pending: state => ({ ...state }),
      success: (state, { name } = {}) => ({
        ...state,
        opened: [
          ...state.opened.filter(x => x.name !== name)
        ]
      }),
      fail: (state, { error } = {}) => ({ ...state, error })
    }
  }
});
// пример вызова
import {
  actions
} from './symbiotes/popups';
// ...
export default connect(
  mapStateToProps,
  dispatch => ({
    onClick: () => {
        dispatch(actions.open.start({ name: PopupNames.Info }));
    }
  })
)(FooComponent);
Из плюсов имеем:
- все в одном файле
- меньше кода
- структурированное представление действий
Из минусов:
- IDE не всегда предлагает подсказки
- сложно искать действие в коде
- сложно переименовать действие
Не смотря на минусы данный модуль успешно используется в наших проектах.
Спасибо LestaD за хорошую работу.
Комментарии (14)
 - jakobz03.03.2019 09:21- Мы как-то сделали себе похожий велик на одном проекте на TypeScript. Получилось ну очень похоже: 
 
 - const initialState: CounterState = { current: 0 } const next = ({ }) => (s: MyState) => ({ ...s, current: s.current + 1 }); const set = (a: { value: number}) => (s: MyState) => ({ ...s, current: s.current + a.value }); const { actionCreators, bindActions, reducer } = buildReducer( initialState, { next, set, }, { prefix: "COUNTER" } );
 
 В отличии от велика из статьи, тут все строго типизировано. Например, типы actionCreator-ов и их параметров выводятся (мы ради этого карировали функции, action => state => state вместо (action, state) => state).
 
 Плюс имена экшнов автоматом строятся, и никаких функций в action-ы не упаковывается. Т.е. в нашем случае actionCreators.set({ value: 10 }) => { type: 'COUNTER_SET', value: 10 }.
 
 Вот тут исходники самой утилитки: gist.github.com/jakobz/ae3e5567e20fff3d66d9e8852a9a655a
 - rgs35003.03.2019 11:36+3- Я позволю себе скопипастить сюда свое сообщение из другой темы: - Итак имеем: Я так и не нашел ответа на вопрос зачем нужны эти псевдоабстракции.
 — Глобальный стор (обычно все глобальное — плохо, а тут вдруг — хорошо).
 — Кучу экшенов где action.type уникален в пределах приложения.
 — Кучу action creator-ов c названием, в большинстве случаев, таким же как action.type, записанным в другом регистре.
 — reducer-ы — по сути обычный switch case.
 
 Теоретически, для уменьшения бойлерплейта вы даже использовали какую-нибудь свистоперделку.
 
 Вопрос:
 Где в этом хозяйстве архитектура, абстракции и изоляции и чем оно координально лучше, чем обычные методы хранилища? ЗЫ: Методы хотя бы можно комбинировать в отличии от словоблудия внутри switch case. - AlexeyAbretov Автор03.03.2019 13:03+1- Спасибо за вопросы. 
 
 Сразу оговорюсь, что статья не об архитектуре в целом, а только об инструменте, который позволяет уменьшить количество кода, повысить читабельность и структурировать часть архитектуры redux.
 
 Не считаю, что глобальный стор или несколько распределенных, а также вызов напрямую методов или через дополнительную инфраструктуру это плохо или хорошо. Каждый использует при разработке, что ему удобнее и подходит для реализации.
 
 Архитектура redux неплохо описана в документации и легко бьется на слои:
 — стор отдельно, что и как сделать со стором отдельно,
 — представление: глупые компоненты, контейнеры, селекторы данных,
 — для перехвата действий и последующего комбинирования действий над стором (и не только), например, redux-saga.
 
 Все это можно легко передавать между проектами и тестировать.
 
 Идеального решения нет, как и ответа на все вопросы. - rgs35003.03.2019 13:18-1- Вы не подумайте что это была претензия к вам и вашей статье. Сам использовал похожие решения и велосипедил ради интереса. Они вполне жизнеспособны, особенно если учесть, что react/redux — достаточно популярные библиотеки. 
 
 
 - DarthVictor03.03.2019 13:36+1- Не понятно как на такие действия подписываться в том же redux-saga? У них можно имя получить, через какой-то условный name? 
 
 будет работать?- yield takeEvery(actions.open.success.name, loadAdditional) - AlexeyAbretov Автор03.03.2019 13:41- yield takeEvery(actions.open.success.toString(), loadAdditional)  - DarthVictor03.03.2019 14:44+1- Тогда ок. А с типизацией в TypeScript или Flow как обстоят дела, не в курсе? 
 
 
 - Aries_ua04.03.2019 18:29- Как вижу подобный код и реализацию — честно, плакать хочется. Ну вот реально вьехать с пол пинка вряд ли получится. Далее все что только можно суем в глобальный стор. Туда жа запихавают логику приложения. Вот попробуйте потом такое приложение оптимизировать. Разбить на слои, где с каждым слоем работала бы команда. 
 
 Почему не вынести бизнес логику получения и обработки данных в отдельные слои? Зачем все держать в сторе, если данные нужны только в одном месте? Как сделать что-то не завязываясь на redux?
 
 В большинстве туториалов пишут о том, что это серебряная пуля, вот берем и радость. Как сказал один девелопер у меня в команде — «чувак, ты не понимаешь?! Это же React-way! Надо только так писать, так в мануалах пишут». Над… В общем, на практике, это просто добавляет проблем, нежели профита.
 
 Что делаю у себя в проектах, это в первую очередь разделяю приложение на слои: бизнес логика, API, компоненты, Store.
 
 — API это набор классов, где реализована коммуникация с сервером
 — бизнес логика, слой где обрабатываются и подготавливаются данные
 — компоненты, по содержат логику только для отрисовки интерфейса, максимально стараемся делать их независимыми, маленькими и тупыми.
 — Store. В сторе держим только те данные, которые нужны нескольким компонентам в одно и тоже время на странице. К примеру, профиль пользователя. Имя пользователя покажем в хедере сайта, в навигационном меню и на странице профиля пользователя. Изменили имя, поменялось в трех точках.
 
 Как видно из описания, нет проблем разделить работы между людьми. Не жестких привязок. В любой момент можно заменить слой на другую технологию, не переписывая все остальное. - LestaD04.03.2019 21:21- Как потом эти бизнес слои связывать вместе используя Flux? 
 Как это дебажить без вменяемых dev-tools?
 
 redux-symbiote был призван решить только одну проблему: убрать бойлерплейт вокруг экшенов и редюссеров, ни больше, ни меньше.
 symbiote — чисто апдейтеры стора, thunk/execute — бизнес-логика, components — отображение данных. Всё тоже самое разделение на слои, только более простое.
 Разделять приложение между командами нужно полноценно разделяя приложение на micro-frontends, а не работать всем вместе в огромном монолите.
 
 А вот по поводу качественного разделения: я постепенно перехожу на effector. Где есть и полноценный дебаг и статическое вычисление зависимых сторов, и красивое API, и отстутствие проблемы ромбовидных зависимостей.
 
 Мб потом статью о нём напишу.
 
 
           
 
Finesse
Как правильно произвести декомпозицию редьюсеров, то есть создать несколько независимых редьюсеров, которые преобразуют независимые состояния, и затем объединить их в один редьюсер? Если создать несколько редьюсеров через redux-symbiote и объединить их через combineReducers, то возникнет риск коллизии названий типов действий (потому что они не записаны в коде явно).
LestaD
Спасибо за вопрос. `createSymbiote` имеет третий параметр namespace/options, с помощью которого можно задать префикс для всех экшен-типов сразу.
github.com/sergeysova/redux-symbiote#options
Некоторые примеры использования symbiote можно посмотреть здесь: github.com/howtocards/frontend/tree/dev/src/features/cards/symbiotes