React и Redux, в последнее время одни из самых популярных buzz-words в мире фронтенда. Поэтому когда мне потребовалось сделать веб-приложение, которое бы отображало данные, полученные с сервера, а также позволяло бы ими манипулировать (создавать, удалять и изменять), я решил построить его на основе связки React и Redux. Множество getting-started руководств покрывают только функционал создания компонентов, action creators и reducers. Но как только дело касается обмена с сервером, начинаются сложности — растет количество необходимых action creator, редьюсеров. Причем они очень похожи друг на друга, с миниальными отличиями. В большинстве случаев — только в типе (имени) активности. После того, как я создал третий одинаковый набор креаторов и редьюсеров, то появилось желание что-то изменить. Так родилась идея реализации redux-redents.


Начало, dictionary reducers


В общем виде reducers очень похожи друг на друга — принять свой action и создать на его основе новое состояние хранилища. Если рассматривать reducers для обработки ответа с сервера, то они будут отличаться только типом action. Так родилась идея "универсального" редьюсера:


function createReducer(acttype,initialState) {
    return (state = initialState, action) => {
        if(action.type!=acttype) return state;
        return action.res.data;
    };
}

const dicts = {
  type1 : createReducer(TYPE1_CONSTANT,{}),
  type2 : createReducer(TYPE2_CONSTANT,[])
}

const rootReducer = combineReducers({...dicts});

Уже это позволяет не писать одинаковые функции и не городить switch-case конструкции.


Константы. Избавление.


Словарь редьюсеров сократил количество одинакового кода, но остались константы для задания типов action. Добавление нового action и его обработчика выглядит так:


  1. создать константу для типа action
  2. создать action creator
  3. создать обработчик createReducer с заданным типом.

Поддержка набора констант начинает раздражать практически сразу. Тем более, что смысла в них практически никакого нет — разработчик их использует только для связки creator и reducer. Таким образом константы можно заменить на соглашения о конфигурировании типов action.
Далее — все action creators для получения данных с сервера выглядят одинаково — создать action с нужным типом и promise для запроса на сервер. Если они выглядят одинаково, то не лучше ли автоматизировать процесс создания creators, а еще лучше сделать универсальный creator?


Объединение двух идей — замена констант соглашениями и универсальный creator и привели рождению модуля.


Соглашения о данных


Если для обмена с сервером используется rest-like api, то для каждого типа данных у нас есть одинаковое число операций по-умолчанию: index (list), get, post, delete; а у каждой операции есть uri и параметры для передачи на сервер. Таким образом можно заключить соглашения об умолчаниях:


  • каждый тип данных поддерживает стандартный набор операций
  • для каждой операции определены правила вычисления url и параметров

Кроме этого нужно предусмотреть возможность расширения:


  • добавление операций
  • конструирование запроса

В результате появился следующий формат:


entities = {
  fruit : {}, //all default
  vegetable: {
    crop : { //custom operation
      type: 'CROP_VEGETABLE',
      request: (data) => return post(url,data) //custom request
    },
    index: {
      url: url1 //custom url
    }
  }
}   

Универсальный action creator


Теперь для упрощения жизни осталось реализовать универсальный actions creator. И снова нам на помощь приходят соглашения:


  • стандартный набор операций — index, get, post, delete
  • правила вычисления url — url = base+entity+'s'
  • правила передачи параметров
    • get,delete — url = url+'/'+data
    • post — отправить data в теле запроса

Универсальный action creator позволяет сделать следующее:


this.props.entityOperation('fruit','index'); //загружает список фруктов
this.props.entityOperation('fruit','get','apple'); //получает фрукт по имени 'apple'
this.props.entityOperation('fruit','post',{name:'orange',id:'5'}); //обновляет или создает 'orange'
this.props.entityOperation('vegetable','crop',{name:'carrot'}); //выполняет операцию crop над parrot

Creator создает action с promise для отправки/получения данных на сервер. Promise нужно как-то обработать и здесь на поиощь приходят redux middleware


Middlewares


Redux middleware — это функция, которая принимает action и следующий в цепочке обработчик и, которая может обработать action сама и/или передать его дальше по цепочке обработчиков. Для обработки promise нам нужно принять исходный action, установить обработчики promise и модифицировать action, чтобы показать что система находится в состоянии запроса данных к серверу. Для модификации можно либо добавлять поля к action, либо модифицировать ее тип. Я выбрал модификацию типа.


Promise Middleware


  • изменяет тип action.type = action.type+'_REQUEST';
  • создает обработчик успеха в promise переслать в следующий обработчик исходный action с ответом сервера
  • создает обработчик ошибки модифицировать тип - action.type=action.type+'_ERROR' и переслать в следующий обработчки ошибку, полученную с сервера
  • возвращает promise

Promises заработали, данные поступают с сервера, но стало не хватать возможности вызвать action после завершения выполнения другого. Например, обновить данные с сервера после сохранения данных на него же. Так была придумана Chain Middleware — функция, которая выполняет action creator после окончания обработки предыдущего action.


Chain Middleware


Для реализации цепочек вызовов, последним параметром к универсальному action creator была добавлена порождающая новый action функция, которая принимает на вход ответ сервера (если он существует) или исходный action (в противном случае).
Порождающая функция вызывается только в том, случае если обрабатываемый action содержит поле status со значением 'done' (action.status=='done')


this.entityOperation('fruit','save',fruit,()=>this.props.entityOperation('fruit','index'));

Module


Естесственным желаением было поделиться этими идеями и их реализацией — так родился модуль redux-redents. Модуль доступен к установке через npm


npm install --save redux-redents

Пример использования


В качестве примера разработано "приложение" client-demo


git clone https://github.com/kneradovsky/redents/
cd client-demo
npm install
npm start

последняя команда соберет приложение, запустит сервер разработки и откроет стартовый URL приложения в браузере
Приложение содержит одну страницу, которая отображает список фруктов и позволяет добавлять, удалять и редактировать фрукты. Вид страницы на скриншоте ниже:


screenshot


Заключение


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

Поделиться с друзьями
-->

Комментарии (10)


  1. braska
    07.07.2016 19:22
    +3

    На самом деле есть прекрасный redux-axios-middleware, который чуть более чем на половину реализует всё то, что умеет Ваш модуль.


    1. qualife
      07.07.2016 19:23

      Возможно, обязательно на него посмотрю. Я не претендую на уникальность. Этот модуль писался под мои нужды, и я подумал, что он еще кому-нибудь может быть интересен.


  1. Fedcomp
    07.07.2016 21:47

    > Для обработки promise нам нужно принять исходный action, установить обработчики promise и модифицировать action, чтобы показать что система находится в состоянии запроса данных к серверу. Для модификации можно либо добавлять поля к action, либо модифицировать ее тип

    В официальных документах кстати очень неплохо сделано: https://github.com/reactjs/redux/blob/master/examples/real-world/middleware/api.js
    у себя в проекте юзал (немного кастомизированный вариант), правда у нас не rest-api. В итоге позволило писать однотипные экшены.


    1. qualife
      07.07.2016 22:20

      Свои middlewares я и писал по мотивам официальной документации. Но идея модуля в том, чтобы НЕ писать ни экшены, ни редьюсеры.


      1. Fedcomp
        08.07.2016 11:52
        +1

        Если у вас не будет actions, то у вас будет разброд и шатание из того что в API юзается, а что уже не нужно (и соответственно вы не будете этого знать). Да и redux без action уже не совсем redux.


        1. qualife
          08.07.2016 12:33

          Экшны есть, просто их не нужно писать. Так же как и редьюсеры. Экшны создаются по конфигурации и соглашениям. Редьюсеры — на основе конфигурации.


          Архитектура редукса не нарушается. Креаторы создают экшны, Мидлваре их передают, редьюсеры меняют состояние.


          1. Fedcomp
            16.07.2016 13:33

            Как вы тогда определяете какой кусок апи вы уже не используете?


            1. qualife
              17.07.2016 12:43

              Я не очень понял вопрос, какого апи и в чем проблема, что он не используется?


  1. rageOfAxe
    08.07.2016 19:20
    +1

    Я могу на основе конфигурации сделать редьюсер с достаточно хитрой логикой, типа композиции map, filter etc?


    1. qualife
      17.07.2016 12:45

      Нет. Такие редьюсеры нужно, к сожалению, самому. Библиотека для простых редьюсеров — состояние как копия экшена.