Асинхронные экшены¶
В базовом руководстве мы построили простое приложение — список дел. Оно было совершенно синхронным. Каждый раз, когда экшен был отправлен, состояние было немедленно обновлено.
В этом руководстве мы будем строить асинхронное приложение, которое будет использовать API Reddit'а для отображения текущих заголовков для выбранного subreddit'a. Так как же асинхронность вписывается в концепцию Redux?
Экшены¶
Когда Вы вызываете асинхронное API, есть два ключевых момента времени: момент непосредственного вызова и момент получения ответа или timeout'a.
Для каждого из этих моментов обычно может требоваться изменение состояния приложения (application state). Для изменения состояния вы должны запустить (dispatch) нормальные экшены, которые будут обработаны редьюсером синхронно. Обычно для любого API запроса вам понадобится запустить (dispatch) по крайней мере три разных вида экшенов?
- Экшен, информирующий редьюсер о том, что запрос начался.
- Редьюсер может обрабатывать такой вид экшена, переключая флаг
isFetching
в состоянии приложения. Именно так UI понимает, что самое время показать лоадер/спиннер. - Экшен, информирующий редьюсер о том, что запрос успешно завершился.
- Редьюсер может обрабатывать такой вид экшена, объединяя полученные из запроса данные с состоянием, которым управляет этот редьюсер и сбрасывая флаг
isFetching
. - Экшен, информирующий редьюсер о том, что запрос завершился неудачей.
- Редьюсер может обрабатывать такой вид экшенов, сбрасывая флаг
isFetching
. Также некоторые редьюсеры могут захотеть сохранить сообщение об ошибке, чтобы UI мог его отобразить.
Для всего этого вы можете использовать поле status
в ваших экшенах:
1 2 3 |
|
Или вы можете объявить отдельные типы для таких экшенов:
1 2 3 |
|
Выбор использования одного типа экшена с флагами или нескольких отдельных типов экшенов остается за вами. Это соглашение, которое вы должны утвердить с вашей командой.
Использование нескольких типов экшенов уменьшают вероятность для ошибки, но это не проблема, если вы генерируете экшены и редьюсеры с помощью таких библиотек, как redux-actions.
Какое бы соглашение вы не выбрали, следуйте ему на протяжении всего приложения.
В этом руководстве мы будем использовать несколько отдельных типов экшенов.
Синхронные генераторы экшенов¶
Давайте начнем с объявления нескольких синхронных типов и генераторов экшенов, которые нам понадобятся в нашем приложении. Тут пользователь может выбрать subreddit для отображения:
actions.js (Synchronous)
1 2 3 4 5 6 7 8 9 10 |
|
Также пользователь может нажать кнопку "обновить" для обновления:
1 2 3 4 5 6 7 8 |
|
Это были экшены отвечающие за взаимодействие с пользователем. Также у нас будет и другой тип экшена, отвечающий за сетевые запросы. Сейчас мы просто определим их, а чуть позже посмотрим, как посылать такие экшены.
Когда нужно будет фетчить посты для какого-нибудь subreddit'a мы будем посылать экшен REQUEST_POSTS
:
1 2 3 4 5 6 7 8 |
|
Важно чтобы этот экшен был отделен от SELECT_SUBREDDIT
и INVALIDATE_SUBREDDIT
. Хотя они, экшены, могут происходить одно за другим, но, с возрастанием сложности приложения вам может понадобится фетчить какие-то данные независимо от действий пользователя, например, для предзагрузки самых популярных subreddit'ов или для того, чтобы изредка обновлять устаревшие данные. Вы можете также захотеть фетчить данные в ответ на смену роута, так что не очень мудро связывать обновление данных с каким-то определенным UI-событием.
Наконец, когда сетевой запрос будет осуществлен, мы отправим экшен RECEIVE_POSTS
:
1 2 3 4 5 6 7 8 9 10 |
|
Это все, что нам нужно знать на текущий момент. Конкретный механизм отправки этих экшенов вместе с сетевыми запросами будет обсуждаться позже.
Обратите внимание на обработчик ошибки
В реальном приложении вам бы также понадобилось отправлять экшен, в случае завершения сетевого запроса ошибкой. В этом руководстве мы не будем реализовывать обработку ошибок.
Разработка структуры стора¶
Как и в базовом руководстве, вам нужно разработать структуру состояния приложения, прежде чем начинать писать само приложение. В случае с асинхронным кодом появляется больше состояний, о которых нужно позаботиться, так что нам нужно все как следует обдумать.
Часто именно эта часть сбивает с толку новичков, потому что сразу не ясно, какая информация описывает состояние в асинхронном приложении и как организовать все это в одно дерево состояния.
Мы начнем с наиболее общего варианта использования: списков. Веб-приложения часто отображают списки чего-либо. Например, список постов или список друзей. Вам нужно будет решить, какие типы списков сможет отображать ваше приложение. Вам нужно хранить их отдельно в состоянии, потому что в этом случае вы можете кешировать их и снова обновлять данные при необходимости.
Вот как может выглядеть структура состояния для нашего приложения “Reddit headlines”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
тут есть несколько важных моментов:
-
Мы храним информацию о каждом subreddit’е отдельно, следовательно мы можем кешировать любой subreddit. Когда пользователь переключается между ними во второй раз, обновление UI будет мгновенным и мы сможем не перезагружать данные, если мы этого не хотим. Не переживайте о том, что все эти элементы (subreddit'ы, а их может быть очень много) будут находиться в памяти: Вам не понадобятся никакие чистки памяти, если только вы и ваш пользователь не имеете дело с десятками тысяч элементов и при этом пользователь очень редко закрывает вкладку браузера.
-
Для каждого списка элементов вы захотите хранить
isFetching
для показа спиннера,didInvalidate
, который вы потом сможете изменить, если данные устареют,lastUpdated
для того чтобы знать, когда данные были обновлены в последний раз, и собственноitems
. В реальном приложении вы также захотите хранить состояние страничной навигации:fetchedPageCount
иnextPageUrl
.
Обратите внимание на вложенные сущности
В этом примере мы храним полученные элементы вместе с информацией о постраничной навигации. Однако этот подход будет очень плох, если у вас будут вложенные сущности, которые ссылаются друг на друга или если Вы дадите пользователю возможность редактировать элементы. Представьте, что пользователь хочет отредактировать загруженный пост, но этот пост сдублирован в нескольких местах в дереве состояния (state tree). Реализация такого будет очень болезненна.
Если у вас есть вложенные сущности или если вы даете возможность редактировать загруженные элементы, то вы должны хранить их отдельно в состоянии, как если бы оно (состояние) было базой данных. И в информации о постраничной навигации вы можете ссылаться на такие элементы только по их ID. Это позволит вам всегда держать их в актуальном состоянии. С таким подходом ваше состояние (state) может выглядеть вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
В этом руководстве мы не будем нормализовывать сущности, но это то, о чем вам стоит задуматься для более динамичного приложения.
Обработка экшенов¶
Перед тем как переходить к деталям отправки экшенов вместе с сетевыми запросами, мы напишем редьюсеры для экшенов, которые определили выше.
Обратите внимание на композицию редьюсеров
Предполагается, что вы понимаете что такое композиция редьюсеров с помощью функции combineReducers()
, описанной в разделе Разделение редьюсеров в базовом руководстве. Если это не так, то, пожалуйста, сначала прочтите это.
reducers.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
Две части этого кода вызывают особый интерес:
- Мы используем ES6-синтаксис вычисляемого свойства, т. е. мы можем обновить
state[action.subreddit]
с помощьюObject.assign()
с использованием меньшего количества строк кода. Вот это:
1 2 3 4 5 6 |
|
эквивалентно этому:
1 2 3 4 5 6 |
|
- Мы извлекли
posts(state, action)
, который управляет состоянием конкретного списка постов. Это просто композиция редьюсеров! Нам выбирать, как разбить/разделить редьюсер на более мелкие редьюсеры и в этом случае, мы доверяем обновление элементов внутри объекта функции-редьюсеруposts
.
Помните, что редьюсеры — это всего лишь функции, т. е. вы можете использовать функциональную композицию (речь о функциональном подходе к программированию и композиции функций) и функции высшего порядка так часто, как вам это будет удобно.
Асинхронные генераторы экшенов¶
Наконец, как мы используем синхронные генераторы экшенов, созданные нами ранее вместе с сетевыми запросами? Стандартный для Redux путь — это использование Redux Thunk middleware. Этот мидлвар (middleware) содержится в отдельном пакете, который называется redux-thunk
. Мы поясним, как работают мидлвары позже. Сейчас есть одна важная вещь, о которой вам нужно знать: при использовании конкретно этого мидлвара, генератор экшенов может вернуть функцию, вместо объекта экшена. Таким образом, генератор экшена превращается в преобразователь (Thunk)
Когда генератор экшена вернет функцию, эта функция будет вызвана мидлваром Redux Thunk. Этой функции не обязательно быть чистой. Таким образом, в ней разрешается инициировать побочные эффекты (side effects), в том числе и асинхронные вызовы API. Также эти функции могут вызывать экшены, такие же синхронные экшены, которые мы отправляли ранее.
Мы все еще можем определить эти специальные thunk-генераторы экшенов внутри нашего actions.js
файла:
actions.js (Asynchronous)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
Примечание по fetch
Мы используем fetch
API в примерах. Это новое API для создания сетевых запросов, которое заменяет XMLHttpRequest
в большинстве стандартных случаев. Поскольку большинство браузеров до сих пор не поддерживают его нативно, мы полагаем, что вы для этого используете библиотеку cross-fetch
:
1 2 |
|
Внутри она использует полифил whatwg-fetch
на клиенте и node-fetch
на сервере, поэтому вам не понадобится менять вызовы API, если вы захотите сделать ваше приложение универсальным.
Помните, что любой полифил fetch
предполагает, что полифил Promise уже присутствует. Самый простой способ убедиться, что вы подключили Promise-полифил — это подключить ES6-полифил Babel во входной точке, прежде чем любой другой код запустится:
1 2 |
|
Как мы добавляем мидлвар Redux Thunk в механизм диспетчера? Для этого мы используем метод applyMiddleware()
из Redux, как показано ниже:
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Хорошая новость о преобразователях: они могут направлять результаты друг другу.
actions.js (с fetch
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
Это позволяет нам писать более сложный поток асинхронного управления постепенно, в то время, как потребляющий код может оставаться таким же довольно долгое время:
index.js
1 2 3 |
|
Примечание о серверном рендеринге
Асинхронные генераторы экшенов особенно удобны для серверного рендеринга. Вы можете создать стор, вызвать отдельный асинхронный генератор экшена, который вызовет другие асинхронные генераторы экшенов для выборки данных для всей части вашего приложения и отрендерит, только после того, как promise его вернет. Затем ваш стор будет полностью гидратирован с состоянием, необходимым для рендеринга.
Thunk middleware — это не единственный путь управления асинхронными экшенами в Redux.
- Вы можете использовать redux-promise или redux-promise-middleware для отправки Promises вместо функций.
- Вы можете использовать redux-observable для отправки Observables
- Вы можете использовать мидлвар redux-saga для создания более комплексных асинхронных экшенов
- Вы можете использовать мидлвар redux-pack для отправки асинхронных экшенов, базирующихся на промисах
- Вы даже можете писать собственные мидлвары, для описания вызовов вашего API.
Решать вам, попробовать несколько вариантов, выбрать конвенции, которые вам нравятся и следовать им, будь то с использованием мидлвара или без него.
Подключение к UI¶
Отправка асинхронных экшенов не отличается от отправки синхронных экшенов, поэтому мы не будем обсуждать это в деталях. Почитайте использование в связке с React для знакомства с использованием Redux с React-компонентами. С полным исходным кодом, обсуждаемым в этом примере, вы можете ознакомится в примере Reddit API
Следующие шаги¶
Прочтите Асинхронный поток для резюмирования того, как асинхронные экшены вписываются в поток Redux.