React – Flux & Redux. Пошаговое Руководство, Часть 2
Часть 2
Во второй части мы рассмотрим создание Редуктора, поговорим о получении состояния нашего приложения. А также коснемся темы объединения Редукторов и отправки Действий.
Простой Редуктор
Теперь, когда мы знаем, как создать экземпляр Redux, который будет содержать в себе состояние нашего приложения, мы можем сосредоточится на функциях редуктора для трансформации этого состояния.
Несколько слов о редукторе и хранилище
Как вы могли заметить – на flux диаграмме из первой части у нас было указано “Store” (хранилище), вместо “Reducer” (редуктор), как можно было ожидать из контекста Redux. Так в чем же отличаются друг от друга Store и Reducer? Это проще, чем можно себе представить: Store хранит ваши данные внутри себя, в то время, как Reducer этого не делает. В обычном flux-подходе, хранилище содержит в себе состояние приложения, а в Redux при каждом вызове редуктора, передается состояние, которое нужно обновить. Таким образом, Redux хранилище становятся как-бы “уменьшающим состояние” хранилищем и поэтому оно было переименовано в редуктор.
Оглавление
- Часть 1
- Часть 2
- 04. Простой Редуктор
- 05. Получение Состояния
- 06. Объединение Редукторов
- 07. Отправка Действия
- Часть 3
- Часть 4
- 12. Практический Пример
- 13. Заключение
Как было сказано ранее, при создании экземпляра Redux, в него можно передать функцию редуктор:
import { createStore } from 'redux';
var store_0 = createStore( () => {} );
Поэтому Redux сможет вызывать эту функцию применительно к состоянию вашего приложения каждый раз, когда произойдет определенное действие. Давайте сделаем небольшое логирование в нашем редукторе:
var reducer = function ( ...args ) {
console.log( 'Редуктор был вызван с аргументами', args )
}
var store_1 = createStore( reducer );
Результат: 'Редуктор был вызван с аргументами [ undefined, { type: '@@redux/INIT' } ]
Вы заметили это? Наш редуктор был вызван даже если мы не отправили ни одного действия… Это потому, что при
инициализации состояния приложения, Redux фактически отправляет действие инициализации (init): { type:
'@@redux/INIT' }
.
После вызова, редуктор передает следующие параметры: (state, action)
. И значение
undefined
в данном случае является очень логичным, так как при инициализации приложения, состояние
не было еще создано.
Но что это за состояние нашего приложения, которое получилось после того, как Redux отправил действие инициализации (init)?
Получение Состояния
Итак, каким же образом мы можем получить состояние от нашего Redux экземпляра?
import { createStore } from 'redux';
var reducer_0 = function ( state, action ) {
console.log( 'reducer_0 был вызван с состоянием', state, 'и действием', action );
}
var store_0 = createStore( reducer_0 );
Результат: reducer_0 был вызван с состоянием undefined и действием { type: '@@redux/INIT' }
Для получения состояния, которое Redux держит для нас, мы используем метод getState()
:
console.log( 'store_0 состояние после инициализации:', store_0.getState() );
Результат: Redux состояние после инициализации: undefined
Почему же состояние нашего приложения после инициализации все еще undefined
? Ну, это естественно,
так как наш редуктор ничего не делает… Помните, как мы описали ожидаемое поведение редуктора в главе
Встречаем
Redux и немного о Состоянии?
Редуктор является подписчиком на действия и это обыкновенная функция, которая принимает текущее состояние ваше приложения, действие и возвращает новое измененное состояние
Наш редуктор прямо сейчас ничего не возвращает, таким образом, состояние нашего приложения – это то, что вернет
reducer()
, следовательно, в данном случае undefined
.
Давайте попробуем отправить начальное состояние нашего приложения, если переданное состояние в редуктор является
undefined
:
var reducer_1 = function ( state, action ) {
console.log( 'reducer_1 был вызван с состоянием', state, 'и действием', action );
if ( typeof state === 'undefined' ) {
return {}
}
return state;
}
var store_1 = createStore( reducer_1 );
Результат: reducer_1 был вызван с состоянием undefined и действием { type: '@@redux/INIT' }
console.log( 'store_1 состояние после инициализации:', store_1.getState() );
Результат: Redux состояние после инициализации: {}
Как и было ожидаемо, состояние, возвращаемое Redux после инициализации – {}
.
Тем не менее благодаря ES6 существует значительно более понятный способ реализации данного паттерна:
var reducer_2 = function ( state = {}, action ) {
console.log( 'reducer_2 был вызван с состоянием', state, 'и действием', action );
return state;
}
var store_2 = createStore(reducer_2);
Результат: reducer_2 был вызван с состоянием {} и действием { type: '@@redux/INIT' }
console.log( 'store_2 состояние после инициализации:', store_2.getState() );
Результат: Redux состояние после инициализации: {}
Возможно вы заметили, что так как мы использовали параметры по умолчанию для state
параметра reducer_2
,
то мы больше не будем получать undefined
в качестве значения состояния в теле нашего редуктора.
Теперь давайте вспомним, что редуктор вызывается только в качестве ответа на отправленное действие и поэтому,
сейчас мы сымитируем модификацию состояния в ответе на действие с типом 'SAY_SOMETHING'
:
var reducer_3 = function ( state = {}, action ) {
console.log( 'reducer_3 был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
default:
return state;
}
}
var store_3 = createStore( reducer_3 );
Результат: reducer_3 был вызван с состоянием {} и действием { type: '@@redux/INIT' }
console.log( 'Redux состояние после инициализации:', store_3.getState() );
Результат: Redux состояние после инициализации: {}
Ничего нового в нашем состоянии не произошло, так как еще не отправили ни одного действия. Но есть несколько важных вещей в приведенном примере, на которые стоит обратить внимание:
- Предполагается, что наше действие содержит
type
иvalue
свойства. Свойствоtype
– по большому счету является соглашением в flux-действиях, а свойствоvalue
может быть чем угодно. - Вы встретите множество паттернов, включающих в себя конструкцию
switch
для ответа в соответствии с принятым действием в вашем редукторе. - При использовании конструкции
switch
никогда не забывайте проdefault: return state;
, иначе вы получите то, что ваш редуктор вернетundefined
и состояние будет потеряно. - Заметьте, как мы вернули состояние с помощью соединения текущего состояния и
{ message: action.value }
, все это благодаря потрясающей возможности ES7 – Расширению Объектов (Object Spread):{ …state, message: action.value }
- Также стоит заметить, что нотация ES7 Расширение Объекта подходит для нашего примера по той причине,
что она производит поверхностное копирование
{ message: action.value }
поверх нашего состояния (state). То есть свойства первого уровня объекта состояния будут полностью перезаписаны свойствами первого уровня{ message: action.value }
. Но если у нас есть сложная / вложенная структура данных, тогда можно выбрать для обработки обновления состояния другие инструменты:- Immutable.js
- Object.assign
- Ручное слияние
- Или что угодно подходящее под ваши нужды и структуру состояния
Redux позволяет использовать любой из этих инструментов, так как он сам по себе является всего лишь контейнером состояния.
И когда мы почти подошли к обработке действий в нашем редукторе, давайте поговорим о множественных редукторах и их объединении.
Объединение Редукторов
Теперь мы начинаем понимать, что же такое редуктор…
var reducer_0 = function ( state = {}, action ) {
console.log( 'reducer_0 был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
default:
return state;
}
}
…но до того, как пойти дальше, нужно разобрать тот случай, когда наш редуктор будет содержать в себе, например, с десяток действий:
var reducer_1 = function ( state = {}, action ) {
console.log( 'reducer_1 был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
}
case 'DO_SOMETHING':
// ...
case 'LEARN_SOMETHING':
// ...
case 'HEAR_SOMETHING':
// ...
case 'GO_SOMEWHERE':
// ...
// и т.д.
default:
return state;
}
}
Становится очевидным, что в одиночную функцию редуктора не стоит помещать все обработчики действий нашего приложения. Конечно же, это сделать вам никто не запретит, но результат будет не слишком удобным в плане поддержки и понимания кода.
К счастью для нас, Redux совершенно не волнуют – один ли редуктор или их дюжина и он поможет нам объединить редукторы, если их несколько.
Объявим два редуктора
var userReducer = function ( state = {}, action ) {
console.log( 'userReducer был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
// другие действия
default:
return state;
}
}
var itemsReducer = function ( state = [], action ) {
console.log( 'itemsReducer был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
// другие действия
default:
return state;
}
}
В таком случае, каждый редуктор будет отвечать за обработку только какой-то определенной части состояния приложения.
Но как мы уже знаем, createStore()
ожидает на вход только одну функцию редуктора.
Как же тогда нам объединить все имеющиеся редукторы? И каким образом объяснить Redux, что каждый редуктор
отвечает только за часть нашего состояния? Это довольно-таки просто. Для этого мы используем специальную
хелпер-функцию combineReducer()
. Эта функция превращает объект, чьи значения являются разными
редукторами в одиночную функцию редуктор, которую можно передать в createStore()
. Полученный
редуктор вызывает каждый дочерний редуктор и собирает результат их работы в одиночный объект состояния. Форма
содержания такого объекта состояния будет совпадать с ключами переданных редукторов.
Короче говоря, вот таким образом можно создать экземпляр Redux с множественными редукторами:
import { createStore, combineReducers } from 'redux';
var reducer = combineReducers({
user: userReducer,
items: itemsReducer
});
Результат:
userReducer был вызван с состоянием {} и действием { type: '@@redux/INIT' }
userReducer был вызван с состоянием {} и действием { type: 'a.2.e.i.j.9.e.j.y.v.i' }
itemsReducer был вызван с состоянием [] и действием { type: '@@redux/INIT' }
itemsReducer был вызван с состоянием [] и действием { type: 'i.l.j.c.a.4.z.3.3.d.i' }
var store_0 = createStore( reducer );
Результат:
userReducer был вызван с состоянием {} и действием { type: '@@redux/INIT' }
itemsReducer был вызван с состоянием [] и действием { type: '@@redux/INIT' }
Как можно заметить, в результате, каждый редуктор был корректно вызван с помощью действия инициализации @@redux/INIT
.
Но что это за второе действие? Это проверка работоспособности, реализованная под капотом combineReducer
для того, чтобы убедиться в том, что редуктор всегда вернет состояние != 'undefined'
. Также обратите
внимание, что первый вызов действия инициализации в combineReducer является, в сущности, случайным
действием для проверки на работоспособность.
console.log( 'store_0 состояние после инициализации:', store_0.getState() );
Результат:
store_0 состояние после инициализации: { user: {}, items: [] }
Будет полезным также отметить, что Redux обрабатывает части нашего состояния таким образом, что конечное
состояние будет иметь вид на основе ключей userReducer
и itemReducer
:
{
user: {}, // {} эта часть возвращена userReducer
items: [] // [] эта часть возвращена itemsReducer
}
К этому моменту у нас есть хорошее понимание того, как должен работать редуктор. Было бы неплохо создать некоторые действия, которые могут быть отправлены и посмотреть их влияние на Redux состояние.
Отправка Действия
Не так давно мы сосредоточили свое внимание на создании нашего редуктора, но до сих пор не отправили ни одного собственного действия. Мы оставим для экспериментов те же редукторы из предыдущей главы и добавим к ним обработку некоторых действий:
var userReducer = function ( state = {}, action ) {
console.log( 'userReducer был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
case 'SET_NAME':
return {
...state,
name: action.name
}
default:
return state;
}
}
var itemsReducer = function ( state = [], action ) {
console.log( 'itemsReducer был вызван с состоянием', state, 'и действием', action );
switch ( action.type ) {
case 'ADD_ITEM':
return [
...state,
action.item
]
default:
return state;
}
}
import { createStore, combineReducers } from 'redux';
var reducer = combineReducers({
user: userReducer,
items: itemsReducer
});
var store_0 = createStore( reducer );
console.log( "\n", '### Начало здесь' );
console.log( 'store_0 состояние после инициализации:', store_0.getState() );
Результат:
store_0 состояние после инициализации: { user: {}, items: [] }
Давайте же отправим наше первое действие… Помните, о чем мы говорили в главе Простой Action Creator:
И как бы сказал Капитан Очевидность, для отправки действия, нам понадобится… функция для отправки действия.
Нужная нам функция отправки предоставляется Redux и она будет распространять наше действие на все редукторы! Эта
функция доступна через свойство экземпляра Redux dispatch
.
Чтобы отправить действие, нужно всего лишь вызвать:
store_0.dispatch({
type: 'AN_ACTION'
});
Результат:
userReducer был вызван с состоянием {} и действием { type: 'AN_ACTION' }
itemsReducer был вызван с состоянием [] и действием { type: 'AN_ACTION' }
Каждый редуктор был вызван, но так как ни один из них не обслуживает это действие, состояние осталось неизменным:
console.log( 'store_0 состояние после действия AN_ACTION:', store_0.getState() );
Результат: store_0 состояние после действия AN_ACTION: { user: {}, items: [] }
Но… подождите-ка минутку! Разве мы не должны использовать action creator для отправки действия? Мы действительно можем использовать actionCreator, но так как все, что он делает – возвращает действие, то это не должно привнести что-нибудь дополнительное в этот пример. Но ради преодоления трудностей в будущем, давайте сделаем все правильно, согласно flux-теории. Создадим action creator и отправим нужное нам действие:
var setNameActionCreator = function( name ) {
return {
type: 'SET_NAME',
name: name
}
}
store_0.dispatch( setNameActionCreator( 'Voldemar' ) );
Результат:
userReducer был вызван с состоянием {} и действием { type: 'SET_NAME', name: ' Voldemar ' }
itemsReducer был вызван с состоянием [] и действием { type: 'SET_NAME', name: ' Voldemar ' }
console.log( 'store_0 состояние после действия SET_NAME:', store_0.getState() );
Результат:
store_0 состояние после действия SET_NAME: { user: { name: 'Voldemar' }, items: [] }
Таким образом, мы только что обработали наше первое действие, и оно изменило состояние нашего приложения!
Но это кажется весьма простым примером и не слишком похоже на случай из реальной жизни. Представьте, что если бы нам захотелось произвести какую-нибудь асинхронную операцию в нашем action creator до отправки действия? Мы поговорим об этом в следующей главе.
И на данный момент, поток нашего приложения будет иметь такой вид:
ActionCreator -> Action -> Dispatcher -> Reducer
При создании статьи были использованы следующие источники: