Redux-Redents — (еще) один модуль для работы с серверными данными из React-Redux приложений.  

    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 репозитории

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      +3
      На самом деле есть прекрасный redux-axios-middleware, который чуть более чем на половину реализует всё то, что умеет Ваш модуль.
        0
        Возможно, обязательно на него посмотрю. Я не претендую на уникальность. Этот модуль писался под мои нужды, и я подумал, что он еще кому-нибудь может быть интересен.
          0

          А вторую половину реализует не менее прекрасный redux-act.

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

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

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

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

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


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

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

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

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

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

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое