Центральный элемент redux - action. Выглядит он примерно так:
{
type: ADD_TODO,
text: 'Build my first Redux app'
}Как мы видим состоит он из:
- action type:
ADD_TODO - данных передваемых при диспатче:
text: 'Build my first Redux app'
Чтобы его создать обычно используется action creator:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}Который использует action type
const ADD_TODO = 'ADD_TODO'Наличие action type требутет FLUX, хранение его отдельной константой бережет нас от опечато. И это все очень здорово в крупных приложениях работает, но на начальных этапах это скорее нам мешает чем помогает.
Давайте для нашего небольшого приложения будем генерировать этот довольно незамысловатый код сами. Генерировать не в рантайме а еще на этапе сборки. Так мы в любой момент может отключить генераторы и оказатся с стандарным "ручным" redux. Так же условимся, что action type всегда будет называтся точно так же как action creator. Так их проще будет найти.
function addTodo(text) {
return text
}Это намного короче. Пока мы ничего не поменяли в логике, просто сделали нудную работу в автоматическом режиме. Вызвать action creator по прежнему можно с помощью
dispatch(addTodo(text))Часто наши экшены асинхронны. Чтобы это было возможно используется Redux Thunk middleware благодоря чему мы можем из экшена возвращать не только обьекты action
return {
type: 'addTodo',
text
}
// Это то что возвращается экшеном после нашего генератора, сами же мы возвращаем просто text как примере вышено и асинхронные функции
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
error => console.log('An error occurred.', error)
)
.then(json =>
dispatch(receivePosts(subreddit, json))
)В таком классическом варианте появляется некоторая путаница.
-
Во первых часть методов синхронная часть асинхронная. Тяжело разобратся в где синхронные а где асинхронные action creator, а где синхронные по их названиям requestPost, fetchPost, receivePost. Понять можно только заглянув внутрь. По этой причине многие команды договариваются ставить особые префиксы в названиях и сваято блюсти codestyle или вовсе хранить их отдельно.
-
При этом надо не забывать что асинхронный вызов должен закончится синхронным (если конечно нам не все равно что ответил бекенд). Есть внегласное правило называть асинхронные методы с особым префиксом, или хранить их в отдельном файле.
-
В коцне концов в общем случае мы вынуждены создавать как минимум два экшена для получения данных с бекенда - экшен которым мы их попросим с бекенда, и синхронный экшен которым мы обновим store (что наглядно показано в примере выше).
-
при этом 99% случаев в одном асинхронном запросе испольуется максимум 3 синхронных:
- Запрос отправлен
- Запрос завершен
- Произошла ошибка
Даже если на начальном этапе от них толку мало, в дальнейшем, в хорошем UX, все они рано или поздно имеют отзыв в интерфейсе в виде прелоадера и сообщений об ошибке.
Так поччему бы не генерировать все три состояния для каждого асинхронного запроса, так же как мы генерируем action creator. По принципу - если метод вернул Promise - нам нужно создать три action:
- methodNameREQUEST(timestamp запроса)
- methodNameERROR(обьект ошибки)
- methodName(ответ бекенда)
Если метод вернул НЕ промис, а что-то иное, мы понимаем это обычный action и делаем обычный dispatch() этого action
- methodName(данные)
В итоге, теперь нам не нужно раз за разом писать этих три одинаковых диспатча, мы можем генерировать одинаковый код автоматически. А это значит что вместо:
export function fetchPosts(subreddit) {
return function (dispatch) {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
error => dispatch(errorInPosts(subreddit, error))
)
.then(json =>
dispatch(receivePosts(subreddit, json))
)
}
}Мы можем писать:
export function fetchPosts(subreddit) {
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
}Из чего сгененируется код вида: (еще не реализовано)
export function asyncfetchPosts(subreddit) {
return function (dispatch) {
dispatch(fetchPostsREQUEST(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
error => dispatch(fetchPostsERROR(subreddit, error))
)
.then(json =>
dispatch(fetchPosts(subreddit, json))
)
}
}Или если мы напишем:
export function fetchPosts(subreddit) {
return { data: 'foo' }
}Cгененируется код вида:
export function syncfetchPosts(subreddit) {
dispatch(fetchPosts(subreddit))
}Кода стало намного меньше, как и путаницы с синхронными / асинхронными методами.
Пожалуйста, покритикуйте мою идею, предложите неучтенные мной кейсы здесь, мы что-нибудь обязательно придумаем!
- У меня синхронный метод, а я хочу сгенерировать ошибку и словить ее в редюсере
- Просто заверните ответ в промис:
return Promise.reject(new Error('упс'));
- Просто заверните ответ в промис:
Все хорошо, но, я бы делал генерацию fetchPostsREQUEST опциональной. Как показала практика, это лучше реализовывать руками по месту необходимости, и лог экшенов становится чище, да и практической необходимости в нем нету.
Не совсем понятно зачем два респонса