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 и его обработчика выглядит так:
- создать константу для типа action
- создать action creator
- создать обработчик 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 в теле запроса
- get,delete —
Универсальный 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 приложения в браузере
Приложение содержит одну страницу, которая отображает список фруктов и позволяет добавлять, удалять и редактировать фрукты. Вид страницы на скриншоте ниже:
Заключение
Буду рад, если мой модуль окажется полезен. Открыт для вопросов, замечаний и предложений по расширению функциональности. Исходный код модуля, как всегда, доступен в GitHub репозитории
Комментарии (10)
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. В итоге позволило писать однотипные экшены.qualife
07.07.2016 22:20Свои middlewares я и писал по мотивам официальной документации. Но идея модуля в том, чтобы НЕ писать ни экшены, ни редьюсеры.
Fedcomp
08.07.2016 11:52+1Если у вас не будет actions, то у вас будет разброд и шатание из того что в API юзается, а что уже не нужно (и соответственно вы не будете этого знать). Да и redux без action уже не совсем redux.
qualife
08.07.2016 12:33Экшны есть, просто их не нужно писать. Так же как и редьюсеры. Экшны создаются по конфигурации и соглашениям. Редьюсеры — на основе конфигурации.
Архитектура редукса не нарушается. Креаторы создают экшны, Мидлваре их передают, редьюсеры меняют состояние.
braska
На самом деле есть прекрасный redux-axios-middleware, который чуть более чем на половину реализует всё то, что умеет Ваш модуль.
qualife
Возможно, обязательно на него посмотрю. Я не претендую на уникальность. Этот модуль писался под мои нужды, и я подумал, что он еще кому-нибудь может быть интересен.