React – Flux & Redux. Пошаговое Руководство, Часть 3
Часть 3
В третье части цикла статей о Flux & Redux мы закончим теоретический разбор жизненного цикла Flux-приложения такими темами, как работа с асинхронными действиями, middleware и подпиской на состояние.
Отправка Асинхронного Действия 1
В прошлой главе мы узнали, как с помощью редуктора можно отправлять действия и каким образом эти действия будут изменять состояние нашего приложения.
Но таким образом мы рассмотрели только синхронные действия или, если выразиться более точно – action creator, который вызывает действия синхронно, то есть, при вызове, действие возвращается немедленно.
Теперь давайте представим простой асинхронный случай:
- Пользователь кликает на кнопке “Скажи: ‘Привет’ через 2 секунды”
- Когда кнопка “А” была нажата, мы бы хотели хотели показать сообщение ‘Привет’ по прошествии двух секунд
- Две секунды спустя, наш шаблон должен быть обновлен сообщением ‘Привет’
Оглавление
- Часть 1
- Часть 2
- 04. Простой Редуктор
- 05. Получение Состояния
- 06. Объединение Редукторов
- 07. Отправка Действия
- Часть 3
- Часть 4
- 12. Практический Пример
- 13. Заключение
Конечно же это сообщение является частью состояния нашего приложения, поэтому мы должны сохранить его в Redux-хранилище. Но что если мы захотим, чтобы наше хранилище сохранило сообщение только спустя две секунды, после того, как action creator был вызван? Ведь если мы обновим наше состояние немедленно, тогда любой подписчик на модификацию состояния, например, наш шаблон – может быть уведомлен об изменении прямо сейчас и среагирует на двухсекундную задержку слишком рано.
И если бы мы вызвали action creator также, как мы это делали до сих пор…
import { createStore, combineReducers } from 'redux';
var reducer = combineReducers({
speaker: function ( state = {}, action ) {
console.log( 'speaker был вызван с состоянием', state, 'и действием', action);
switch ( action.type ) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state;
}
}
})
var store_0 = createStore( reducer );
var sayActionCreator = function ( message ) {
return {
type: 'SAY',
message
}
}
console.log( "\n", 'Запускаем обычный action creator:', "\n" );
console.log( new Date() );
store_0.dispatch( sayActionCreator( 'Привет' ) );
console.log( new Date() );
console.log( 'store_0 состояние после действия SAY:', store_0.getState() );
Результат (пропуская результат инициализации):
Sun Aug 02 2016 01:03:05 GMT+0200 (CEST)
speaker был вызван с состоянием {} и действием { type: 'SAY', message: 'Привет' }
Sun Aug 02 2016 01:03:05 GMT+0200 (CEST)
store_0 состояние после действия SAY: { speaker: { message: 'Привет' } }
…то увидели бы, что наше хранилище было немедленно обновлено.
Вместо этого, мы бы хотели, чтобы action creator был чем-то похож на такой код:
var asyncSayActionCreator_0 = function ( message ) {
setTimeout( function () {
return {
type: 'SAY',
message
}
}, 2000 )
}
Но когда наш action creator не возвращает действие, то он вернет undefined
. Очевидно, что это не совсем то
решение, которое мы ищем.
А весь трюк заключается в том, что вместо того, чтобы возвращать действие, мы вернем функцию. И эта функция будет отправлять действие именно в подходящий для этого момент. Но если мы хотим того, чтобы эта функция могла отправлять действие, то ей должна быть передана функция отправки. Таким образом, это должно выглядеть так:
var asyncSayActionCreator_1 = function ( message ) {
return function ( dispatch ) {
setTimeout( function () {
dispatch({
type: 'SAY',
message
})
}, 2000 )
}
}
И снова, как вы могли заметить – наш action creator возвращает не действие, а функцию. Поэтому существует большой шанс, что наши редукторы не будут знать, что им делать со всем этим. Так давайте выясним, что же происходит…
Отправка Асинхронного Действия 2
Попробуем запустить первый асинхронный action creator ранее нами созданный:
import { createStore, combineReducers } from 'redux';
var reducer = combineReducers({
speaker: function ( state = {}, action ) {
console.log( 'speaker был вызван с состоянием', state, 'и действием', action);
switch ( action.type ) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state;
}
}
})
var store_0 = createStore( reducer );
var asyncSayActionCreator_1 = function ( message ) {
return function ( dispatch ) {
setTimeout( function () {
dispatch({
type: 'SAY',
message
})
}, 2000 )
}
}
console.log( "\n", 'Запускаем наш асинхронный action creator:', "\n" );
store_0.dispatch( asyncSayActionCreator_1( 'Привет' ) );
Результат:
...
/Users/classtar/Codes/redux-tutorial/node_modules/redux/node_modules/invariant/invariant.js:51
throw error;
^
Error: Invariant Violation: Actions must be plain objects. Use custom middleware for async actions.
Кажется, функция даже не достигла наших редукторов. Но Redux достаточно благожелательно относится к возникающим ошибкам и
дает нам подсказку: Use custom middleware for async actions
- Используйте собственный
middleware
для асинхронных действий. Все выглядит так, как будто мы находимся на правильном пути, но, всё-таки – что же
такое это “middleware”?
И чтобы успокоить вас – наш action creator asyncSayActionCreator_1
был полностью правильно написан и будет
работать, так, как это от него ожидали, но только после выяснения того, что же такое middleware и как его
использовать.
Middleware
Мы оставили главу Отправка Асинхронного Действия 2 с новой для нас концепцией – middleware. И каким-то образом middleware должно помочь нам в обработке асинхронных действий. Итак, что же такое middleware?
По большому счету, middleware – это то, что находится между А и Б частями приложения и преобразует данные части А до того, как отправить их в часть Б. Таким образом, вместо:
А -----> Б
Мы имеем такую ситуацию:
А ---> middleware 1 ---> middleware 2 ---> middleware 3 --> ... ---> Б
Как же middleware может помочь нам в контексте Redux? Ну, кажется, что та функция, которую мы возвращаем из нашего асинхронного action creator не может быть изначально обработана с помощью Redux, но если бы мы имели middleware между нашим action creator и редукторами, тогда мы смогли бы преобразовать эту функцию в что-то подходящее для Redux:
Действие ---> dispatcher ---> middleware 1 ---> middleware 2 ---> Редуктор
Наше middleware будет вызываться каждый раз, когда действие (или что угодно еще, например, функция в нашем асинхронном action creator) было отправлено и оно должно помочь нашему action creator отправить существующее действие, когда он того захочет, или, если это желаемое поведение – ничего не делать.
В Redux, middleware являются функцией, которая должна соответствовать очень конкретному определению и строгой структуре:
var anyMiddleware = function ({ dispatch, getState }) {
return function( next ) {
return function ( action ) {
// Ваш код, относящийся к middleware
}
}
}
Следуя приведенному коду, middleware состоит из трех вложенных функций, которые будут вызваться последовательно:
- Функция на первом уровне предоставляет
dispatch
функцию отправки иgetState
функцию, если ваше middleware или action creator нуждается в данных состояния - На втором уровне, функция предоставляет
next
функцию, которая позволит вам передать измененные входные данные в следующее middleware или в Redux, который, после этого, сможет вызвать все редукторы - Функция на третьем уровне предоставляет действие (action) принятое от предыдущего middleware или от вашей функции отправки, и может запустить следующее middleware – продолжить поток действия, или обработать действие соответствующим образом
Middleware, которое необходимо нам создать для асинхронного action creator, называется thunk middleware (переходник, преобразователь) и его код находится здесь: https://github.com/gaearon/redux-thunk. Вот как оно выглядит, переписанное в ES5 для лучшей читаемости:
var thunkMiddleware = function ({ dispatch, getState }) {
// console.log( 'Входим в thunkMiddleware' );
return function( next ) {
// console.log( 'Функция "next" предоставляет:', next );
return function ( action ) {
// console.log( 'Обработка действия:', action );
return typeof action === 'function' ?
action( dispatch, getState ) :
next ( action )
}
}
}
Чтобы объяснить Redux, что мы имеем одно или более middleware, мы должны использовать одну из Redux
хелпер-функций applyMiddleware()
.
Эта функция принимает в качестве параметров все ваши middleware и возвращает функцию, которая вызовется в Redux
createStore
. Когда эта последняя функция будет вызвана, она произведет “хранилище более высокого порядка, которое
применит middleware к функции отправки хранилища” (из
https://github.com/reactjs/redux/blob/v1.0.0-rc/src/utils/applyMiddleware.js).
Пример того, как можно было бы встроить middleware в Redux хранилище:
import { createStore, combineReducers, applyMiddleware } from 'redux';
const finalCreateStore = applyMiddleware( thunkMiddleware )( createStore );
// Для нескольких middleware можно написать так: applyMiddleware( middleware1, middleware2, ... )( createStore );
var reducer = combineReducers({
speaker: function ( state = {}, action ) {
console.log( 'speaker был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
case 'SAY':
return {
...state,
message: action.message
}
default:
return state
}
}
})
const store_0 = finalCreateStore( reducer );
Результат:
speaker был вызван с состоянием {} и действием { type: '@@redux/INIT' }
speaker был вызван с состоянием {} и действием { type: '@@redux/PROBE_UNKNOWN_ACTION_s.b.4.z.a.x.a.j.o.r' }
speaker был вызван с состоянием {} и действием { type: '@@redux/INIT' }
Теперь, когда у нас есть экземпляр хранилища с подготовленным middleware, давайте попробуем снова отправить наше асинхронное действие:
var asyncSayActionCreator_1 = function ( message ) {
return function ( dispatch ) {
setTimeout( function () {
console.log( new Date(), 'Отправляем действие:' );
dispatch({
type: 'SAY',
message
})
}, 2000 )
}
}
console.log( "\n", new Date(), 'Запускаем наш асинхронный action creator:', "\n" );
store_0.dispatch( asyncSayActionCreator_1( 'Привет' ) );
Результат:
Mon Aug 03 2016 00:01:20 GMT+0200 (CEST) Запускаем наш асинхронный action creator:
Mon Aug 03 2016 00:01:22 GMT+0200 (CEST) Отправляем действие:
speaker был вызван с состоянием {} и действием { type: 'SAY', message: 'Привет' }
В результате наше действие было корректно отправлено спустя две секунды после вызова асинхронного action creator!
Любопытства ради, вот как может выглядеть middleware для логирования всех отправленных действий:
function logMiddleware ({ dispatch, getState }) {
return function( next ) {
return function ( action ) {
console.log( 'logMiddleware полученное действие:', action );
return next(action);
}
}
}
Подобно выше написанному, можно создать middleware для отмены всех действий, проходящих через это middleware. Может быть это будет не слишком полезным, но можно создать небольшую логику работы выборочной отмены некоторых действий и для передачи других в следующее middleware или Redux:
function discardMiddleware ({ dispatch, getState }) {
return function( next ) {
return function ( action ) {
console.log( 'discardMiddleware полученное действие:', action );
}
}
}
Попробуйте изменить finalCreateStore
приведенное выше используя logMiddleware
и / или
discardMiddleware
и
посмотрите, что произойдет… Например, применив:
const finalCreateStore = applyMiddleware( discardMiddleware, thunkMiddleware )( createStore );
Ваши действия никогда не достигнут thunkMiddleware
и даже ни одного из редукторов.
Множество других хороших примеров использования middleware можно найти по ссылке http://redux.js.org/docs/introduction/Ecosystem.html.
В качестве промежуточного итога, давайте рассмотрим то, что мы изучили:
- Как создавать действия и action creators
- Как отправлять наши действия
- Как обрабатывать с помощью middleware наши собственные действия, например, асинхронного типа
И теперь осталась единственная часть цикла Flux-приложения, которую мы еще не рассмотрели – уведомления об обновлении состояния и реакция на них, например, повторным рендером наших компонентов.
Итак, как же мы можем подписаться на обновления нашего Redux-хранилища?
Подписчик на Состояние
Мы уже достаточно близко подошли к завершению Flux-цикла, но все еще упускаем одну критически важную часть:
Без этого, мы не сможем, например, обновить наш шаблон при изменениях в хранилище.
К счастью, существует очень простой способ “наблюдать” за обновлениями Redux-хранилища:
store.subscribe( function() {
// Получаем последнее состояние хранилища здесь, например:
console.log( store.getState() );
});
Да… Это настолько просто, что можно начать верить в чудеса и Деда Мороза!
Давайте попробуем это в деле:
import { createStore, combineReducers } from 'redux';
var itemsReducer = function ( state = [], action ) {
console.log( 'itemsReducer был вызван с состоянием', state, 'и действием', action );
switch (action.type) {
case 'ADD_ITEM':
return [
...state,
action.item
]
default:
return state;
}
}
var reducer = combineReducers({ items: itemsReducer });
var store_0 = createStore( reducer );
store_0.subscribe( function() {
console.log( 'store_0 было обновлено. Последнее состояние хранилища:', store_0.getState(); );
// Обновите свой шаблон здесь
})
var addItemActionCreator = function( item ) {
return {
type: 'ADD_ITEM',
item: item
}
}
store_0.dispatch( addItemActionCreator({ id: 1234, description: 'что угодно' }) );
Результат:
...
store_0 было обновлено. Последнее состояние хранилища: { items: [ { id: 1234, description: 'что угодно' } ] }
Наш подписчик был корректно вызван и наше хранилище содержит новое значение нами добавленное.
Теоретически мы бы могли остановиться здесь. Наш цикл Flux закрыт, мы изучили все понятия и идеи из которых состоит Flux и увидели, что не так уж тут много магии. Но честно говоря, есть еще несколько вещей, которые можно было бы обсудить, и мы их преднамеренно пропустили в последнем примере ради простоты понимания:
- Наш Подписчик не принимает состояние в качестве параметра, почему?
- Так как мы не принимаем наше новое состояние, мы были вынуждены использовать хранилище в замыкании
store_0
, таким образом, данное решение не слишком подходит к реальному приложению со множеством модулей… - Как на самом деле происходит обновление наших шаблонов?
- Как нам отписаться от обновлений хранилища?
- И в целом – как мы должны объединить Redux с React?
Теперь мы приступим к более специфичной области использования Redux внутри React.
Очень важно понимать, что Redux никаким образом не связан с React. Чем он является на самом деле, так это “контейнером предсказуемого состояния для JavaScript приложений” и вы можете использовать его самыми разнообразными способами, например, в React-приложении, которое является всего лишь одним из способов использования Redux.
Учитывая данное обстоятельство, мы могли бы немного запутаться относительно использования Redux с React, если бы не предварительно созданный официальный инструмент для связки этих двух библиотек react-redux. Этот репозиторий содержит все необходимые связки для упрощения нашей жизни при работе с Redux внутри React.
Возвращаясь к случаю с нашим Подписчиком… Почему у нас получилась именно эта функция Подписчика, которая кажется такой простой, но в тоже время не предоставляет необходимого функционала?
На самом деле, в этой простоте заключается огромная сила! Redux с его текущим минималистичным API (включая Подписчика) является весьма расширяемым и это позволяет разработчикам добиваться невероятных результатов, создавая, например, такие продукты, как Redux DevTools.
Но все-таки нам нужно более подходящее API для подписки на изменения нашего хранилища. Именно это даст нам
react-redux: API, которое позволит заполнить пробел между “сырым” механизмом подписки в Redux и ожиданиями
разработчика. В итоге вам не придется использовать Подписчика напрямую. Вместо этого, вы будете использовать
функции для связи, такие, как provide
или connect
и они возьмут на себя всю работу с методами подписки.
Таким образом, методы подписки будут использоваться, но через обертку в виде дополнительно слоя API, который предоставит нам доступ к Redux состоянию.
На данный момент мы рассмотрели необходимые связи, а также простой способ связывания наших компонентов, и Redux состояния. В следующей части мы разберем весь изученный материл на практическом примере.
При создании статьи были использованы следующие источники: