Создание actions
Наконец-то мы подходим к вопросу взаимодействия пользователя с приложением. Практически любое действие пользователя в интерфейсе = отправка действия (dispatch actions)
В нашем приложении по клику на кнопку года мы должны:
  • установить заголовок
  • загрузить фото этого года из VK
Сейчас предлагаю рассмотреть установку заголовка, так как загрузка фото требует выполнения асинхронного запроса, а чтобы добраться до этого, мы должны рассмотреть несколько интересных вещей. К тому же, установка заголовка отлично показывает на простом примере, как вращаются данные внутри redux-приложения, а именно:
  1. 1.
    Приложение получило начальное состояние (initial state)
  2. 2.
    Пользователь нажав кнопку, отправил действие (dispatch action)
  3. 3.
    Соответсвующий редьюсер обновил часть приложения, в согласии с тем, что узнал от действия.
  4. 4.
    Приложение изменилось и теперь отражает новое состояние.
  5. 5.
    ... (все повторяется по кругу, с пункта 2)
Это и есть однонаправленный поток данных.
Создадим page actions (действия для сущности page):
src/actions/PageActions.js
1
export function setYear(year) {
2
return {
3
type: 'SET_YEAR',
4
payload: year,
5
}
6
}
Copied!
Напоминаю, что поле type - обязательное, а payload - "негласное" соглашение. Немного об этом, можно почитать на английском тут.
Научим редьюсер page реагировать на наше действие:
src/reducers/page.js
1
const initialState = {
2
year: 2018,
3
photos: [],
4
}
5
6
export function pageReducer(state = initialState, action) {
7
switch (action.type) {
8
case 'SET_YEAR':
9
return { ...state, year: action.payload }
10
11
default:
12
return state
13
}
14
}
Copied!
Обратите внимание, в аргументах у функции page указан второй аргумент - action. Это стандартные аргументы redux reducer'а. Благодаря этому, мы можем обрабатывать различные действия по их типу, попадая в нужную секцию case оператора switch.
Так же обратите внимание, что мы не изменили объект state, а вернули новый с полем year равным action.payload (а значит годом, выбранным пользователем, который был послан в action.payload).

Добавляем вызов actions из компонентов

(возможно, вы будете замечать у меня или в других руководствах, что говорят: стейт (state) приложения - это тоже самое, что и "стор" (store) приложения. Во втором издании учебника я везде стараюсь писать store, чтобы вы не путались со стейтом реакт компонента)
У нас есть action, и есть reducer готовый изменить store приложения. Но наш компонент не знает как обратиться к необходимому действию.
Согласно таблице из прошлого раздела: для изменения данных, наш компонент <Page />, должен вызывать callback из this.props, а наш контейнер* <App /> - отправлять действие (dispatch action).
* - я говорю, контейнер, хотя правильнее называть контейнером <Connect(App) />, но так как он генерируется функцией connect на основе App.js, считаю это допустимым.
Из документации функции connect, мы видим, что с помощью этой функции можно не только подписаться на обновления данных (mapStateToProps), но и "прокинуть" наши actions в контейнер (mapDispatchToProps).
connect, первым аргументом принимает "маппинг" (соответствие) state к props, а вторым маппинг dispatch к props. Как бы дико это не звучало, на практике это значит, что нам достаточно передать второй аргумент.
Исправим App.js
src/containers/App.js
1
import React, { Component } from 'react'
2
import { connect } from 'react-redux'
3
import { User } from '../components/User'
4
import { Page } from '../components/Page'
5
import { setYear } from '../actions/PageActions'
6
7
import './App.css'
8
9
class App extends Component {
10
render() {
11
const { user, page, setYearAction } = this.props
12
return (
13
<div className="App">
14
<header className="App-header">
15
<h1 className="App-title">Мой топ фото</h1>
16
</header>
17
<User name={user.name} />
18
<Page photos={page.photos} year={page.year} setYear={setYearAction} />
19
</div>
20
)
21
}
22
}
23
24
const mapStateToProps = store => {
25
return {
26
user: store.user,
27
page: store.page,
28
}
29
}
30
31
const mapDispatchToProps = dispatch => {
32
return {
33
setYearAction: year => dispatch(setYear(year)), [1]
34
}
35
}
36
37
export default connect(
38
mapStateToProps,
39
mapDispatchToProps
40
)(App)
Copied!
Начнем с разбора mapDispatchToProps. Данная функция, первым аргументом получает dispatch, а значит мы можем теперь "диспатчить" экшены, которые будут пойманы редьюсером. Еще раз:только те экшены, которые были отправлены с помощью "диспетчера" будут пойманы редьюсером.
Затем мы внутри mapDispatchToProps вернули объект, который в итоге приклеится в this.props (так же, как и было в mapStateToProps).
И в конце, мы решили в "приклееном объекте" создать функцию setYearAction [1], суть которой сводится к следующему: "диспатчни" импортированный выше setYear с переданным годом.
Обычно пишут без добавления Action: setYear: year => dispatch(setYear(year)), но я хотел бы уменьшить путаницу для тех людей, кто не силен в основах JavaScript (а зря!) и сейчас может быстро запутаться.
Так же я пишу return, для того, чтобы вы могли удобно сконсолить значения аргументов, если вам что-то не понятно. Без return, можно написать так:
1
const mapDispatchToProps = dispatch => ({
2
setYearAction: year => dispatch(setYear(year)),
3
})
Copied!
После выполнения connect(mapStateToProps, mapDispatchToProps)(App), мы получили в <App /> новые свойства (props), что наглядно демонстрирует вкладка "React" в chrome dev tools.
setYearAction-in-mapDispatchToProps
Добавив setYear в свойства <Page />, не составит труда использовать необходимый action из компонента, который по прежнему знать ничего не знает о redux.
Добавим несколько кнопок с годами и обработчик клика на них, в котором будем считывать название года с самой кнопки и отправлять его с помощью экшена 'SET_YEAR' прямиком в редьюсер page.
src/components/Page.js
1
import React from 'react'
2
import PropTypes from 'prop-types'
3
4
export class Page extends React.Component {
5
onBtnClick = e => {
6
const year = +e.currentTarget.innerText
7
this.props.setYear(year)
8
}
9
render() {
10
const { year, photos } = this.props
11
return (
12
<div>
13
<div>
14
<button onClick={this.onBtnClick}>2018</button>
15
<button onClick={this.onBtnClick}>2017</button>
16
<button onClick={this.onBtnClick}>2016</button>
17
<button onClick={this.onBtnClick}>2015</button>
18
<button onClick={this.onBtnClick}>2014</button>
19
</div>
20
<p>
21
У тебя {photos.length} фото за {year} год
22
</p>
23
</div>
24
)
25
}
26
}
27
28
Page.propTypes = {
29
year: PropTypes.number.isRequired,
30
photos: PropTypes.array.isRequired,
31
setYear: PropTypes.func.isRequired, // добавили новое свойство в propTypes
32
}
Copied!
Сейчас если кликнуть на кнопку с годом, то в приложении год будет изменяться. Вау?)
Что происходит: по клику на кнопку, вызывается переданное в свойствах функция, в которой диспатчится экшен (с типом SET_YEAR и годом). Затем, так как этот экшен был "диспатчнут" он пролетает через все редьюсеры (у нас их два: user и page). Так как в page есть case 'SET_YEAR' - редьюсер возвращает новое состояние, а именно - берет все что было в нашем state (по факту - все что было в данном "куске пирога" от стора связанное с page) и возвращает новое значение года:
1
case 'SET_YEAR':
2
return { ...state, year: action.payload }
Copied!
Затем, так как год изменился, в компоненте <App /> случилось обновление, так как внутри mapStateToProps мы подписаны на изменение данных из редьюсера page. Раз случилось обновление, значит перерисовались все потомки и в том числе, в компонент <Page /> ушло новое значение года.
p.s. в коде было использовано свойство DOM-элемента textContent
p.p.s. можете добавить console.log(store) в mapStateToProps и посмотреть есть ли новые данные.
1
const mapStateToProps = store => {
2
console.log(store)
3
return {
4
user: store.user,
5
page: store.page,
6
}
7
}
Copied!
change-year-action
Глава выдалась достаточно длинной, а хуже всего, что мы написали "кипу" кода, всего лишь для обновления цифры в заголовке. Где профит, как говорится?
Профит обнаружится дальше, когда ваше приложение разрастется. Когда его будет необходимо поддерживать и добавлять новые фичи. За счет однонаправленного потока данных (юзер кликнул - действие вызвалось - редьюсер изменил состояние - компонент отрисовал изменения) даже в приложении, написанном давно, у вас получится очень быстро разобраться и внести необходимые обновления, которые требует бизнес.
Итого: мы научились обновлять Redux-приложение правильно: диспатчить экшен и реагировать на экшен в редьюсере.