Асинхронные actions
Давайте представим синхронное действие:
  • Пользователь кликнул на кнопку
  • dispatch action {type: ТИП_ДЕЙСТВИЯ, payload: доп.данные}
  • интерфейс обновился
Давайте представим асинхронное действие:
  • Пользователь кликнул на кнопку
  • dispatch action {type: ТИП_ДЕЙСТВИЯ_ЗАПРОС}
  • запрос выполнился успешно
    • dispatch action {type: ТИП_ДЕЙСТВИЯ_УСПЕШНО, payload: доп.данные}
  • запрос выполнился неудачно
    • dispatch action {type: ТИП_ДЕЙСТВИЯ_НЕУДАЧНО, error: true, payload: доп.данные ошибки}
Благодаря такой схеме, в reducer'e мы сможем реализовать подобное:
1
switch(тип_действия)
2
case ТИП_ДЕЙСТВИЯ_ЗАПРОС:
3
покажи preloader
4
case ТИП_ДЕЙСТВИЯ_УСПЕШНО:
5
скрой preloader, покажи данные
6
case ТИП_ДЕЙСТВИЯ_НЕУДАЧНО:
7
скрой preloader, покажи ошибку
Copied!
Как нам известно, действие - это простой объект, который возвращается функцией его создающей (action creator).
Убедимся в этом:
src/actions/PageActions.js
1
export const SET_YEAR = 'SET_YEAR'
2
3
export function setYear(year) {
4
return {
5
type: SET_YEAR,
6
payload: year,
7
}
8
}
Copied!
Было бы неплохо иметь возможность возвращать не простой объект, а функцию, внутри которой иметь доступ к методу dispatch, чтобы можно было диспатчить события в момент, когда они совершились. Псевдокод, мог бы выглядеть так:
1
export function getPhotos(year) {
2
return (dispatch) => {
3
dispatch({
4
type: GET_PHOTOS_REQUEST
5
})
6
7
$.ajax(url)
8
.success(
9
dispatch({
10
type: GET_PHOTOS_SUCCESS,
11
payload: response.photos
12
})
13
)
14
.error(
15
dispatch({
16
type: GET_PHOTOS_FAILURE,
17
payload: response.error,
18
error: true
19
})
20
)
21
}
22
}
Copied!
Но вот незадача, actions - это простой объект, и если action creator возвращает не простой объект, а функцию, то это как-то... Подождите! Ведь это именно то, что нам нужно: Если action creator возвращает не простой объект, а функцию - выполни ее, иначе если это простой объект ... тадам, передай дальше. Более того, мы знаем, что в цепочке middleware у нас как раз есть доступный метод dispatch! И еще бонусом getState.
Отлично, мы только что поняли, что нам нужен еще один усилитель. Такой усилитель уже написан, причем код его невероятно прост, я даже приведу его здесь:
усилитель: redux-thunk
1
function createThunkMiddleware(extraArgument) {
2
return ({ dispatch, getState }) => next => action => {
3
if (typeof action === 'function') {
4
return action(dispatch, getState, extraArgument);
5
}
6
7
return next(action);
8
};
9
}
10
11
const thunk = createThunkMiddleware();
12
thunk.withExtraArgument = createThunkMiddleware;
13
14
export default thunk;
Copied!
Нам остается лишь добавить зависимость в наш проект.
1
npm install redux-thunk --save
Copied!
И добавить redux-thunk в цепочку усилителей перед логгером, так как логгер должен быть последним усилителем в цепочке.
1
import { createStore, applyMiddleware } from 'redux'
2
import { rootReducer } from '../reducers'
3
import logger from 'redux-logger'
4
import thunk from 'redux-thunk'
5
6
export const store = createStore(rootReducer, applyMiddleware(thunk, logger))
Copied!
Для практики, предлагаю написать следующее:
  • по клику на кнопку с номером года
    • меняется год в заголовке
    • ниже (где должны быть фото), появляется текст "Загрузка..."
  • после удачной загрузки*
    • убрать текст "Загрузка..."
    • отобразить строку "У тебя ХХ фото" (зависит, от длины массива, переданного в action.payload)
* вместо реального метода загрузки, будем использовать setTimeout, который является удобным для тренировок исполнения асинхронных запросов.
Вы можете попробовать выполнить это задание сами, а потом сравнить его с решением ниже.
Для отображения / скрытия фразы "Загрузка...", используйте в reducer'е еще одно свойство у состояния. Например, isFetching:
1
const initialState = {
2
year: 2016,
3
photos: [],
4
isFetching: false
5
}
Copied!
Решение ниже.
Изменим action creator: src/actions/PageActions.js
1
export const GET_PHOTOS_REQUEST = 'GET_PHOTOS_REQUEST'
2
export const GET_PHOTOS_SUCCESS = 'GET_PHOTOS_SUCCESS'
3
export function getPhotos(year) {
4
return dispatch => {
5
// экшен с типом REQUEST (запрос начался)
6
// диспатчится сразу, как будто-бы перед реальным запросом
7
dispatch({
8
type: GET_PHOTOS_REQUEST,
9
payload: year,
10
})
11
12
// а экшен внутри setTimeout
13
// диспатчится через секунду
14
// как будто-бы в это время
15
// наши данные загружались из сети
16
setTimeout(() => {
17
dispatch({
18
type: GET_PHOTOS_SUCCESS,
19
payload: [1, 2, 3, 4, 5],
20
})
21
}, 1000)
22
}
23
}
Copied!
Изменим reducer: src/reducers/page.js
1
import { GET_PHOTOS_REQUEST, GET_PHOTOS_SUCCESS } from '../actions/PageActions'
2
3
const initialState = {
4
year: 2018,
5
photos: [],
6
isFetching: false, // изначально статус загрузки - ложь
7
// так как он станет true, когда запрос начнет выполнение
8
}
9
10
export function pageReducer(state = initialState, action) {
11
switch (action.type) {
12
case GET_PHOTOS_REQUEST:
13
return { ...state, year: action.payload, isFetching: true }
14
15
case GET_PHOTOS_SUCCESS:
16
return { ...state, photos: action.payload, isFetching: false }
17
18
default:
19
return state
20
}
21
}
Copied!
У нас готова логика для обновления состояния (и интерфейса, разумеется). Осталось поправить отображение.
Так как мы переписали и переименовали функцию (setYear -> getPhotos):
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 { getPhotos } from '../actions/PageActions'
6
7
class App extends Component {
8
render() {
9
const { user, page, getPhotosAction } = this.props
10
return (
11
<div className="app">
12
<Page
13
photos={page.photos}
14
year={page.year}
15
isFetching={page.isFetching}
16
getPhotos={getPhotosAction}
17
/>
18
<User name={user.name} />
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
getPhotosAction: year => dispatch(getPhotos(year)),
34
}
35
}
36
37
export default connect(
38
mapStateToProps,
39
mapDispatchToProps
40
)(App)
Copied!
Обновим соответствующий компонент:
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.getPhotos(year) // setYear -> getPhotos
8
}
9
render() {
10
const { year, photos, isFetching } = this.props // вытащили isFetching
11
return (
12
<div className="ib page">
13
<p>
14
<button className="btn" onClick={this.onBtnClick}>
15
2018
16
</button>{' '}
17
<button className="btn" onClick={this.onBtnClick}>
18
2017
19
</button>{' '}
20
<button className="btn" onClick={this.onBtnClick}>
21
2016
22
</button>{' '}
23
<button className="btn" onClick={this.onBtnClick}>
24
2015
25
</button>{' '}
26
<button className="btn" onClick={this.onBtnClick}>
27
2014
28
</button>
29
</p>
30
<h3>{year} год</h3>
31
{/* добавили отрисовку по условию */}
32
{isFetching ? <p>Загрузка...</p> : <p>У тебя {photos.length} фото.</p>}
33
</div>
34
)
35
}
36
}
37
38
Page.propTypes = {
39
year: PropTypes.number.isRequired,
40
photos: PropTypes.array.isRequired,
41
getPhotos: PropTypes.func.isRequired, // setYear -> getPhotos
42
// добавили новое свойство - isFetching, причем в propTypes нет boolean, есть bool
43
isFetching: PropTypes.bool.isRequired,
44
}
Copied!
Когда будете проверять работу в браузере, обратите внимание на логгер. Он все так же работает и информативен.
async-request-setTimeout
Пока мы писали код для асинхронного запроса, мы НЕ нарушили главные принципы redux-приложения:
  1. 1.
    Мы всегда возвращали новое состояние (новый объект, смотрите src/reducers/page.js)
  2. 2.
    Мы строго следовали однонаправленному потоку данных в приложении: юзер кликнул - возникло действие - редьюсер изменил - компонент отобразил.
Итого: вы можете сами дописать наше приложение, чтобы оно взаимодействовало с VK, так как все что нужно, это добавить реальный асинхронный запрос (точнее парочку - для логина, и для получения фото). Для этого придется почитать документацию по работе с VK API.
Для тех, кто хочет добить пример поскорее - следующая глава, в которой мы загрузим таки реальные фото из вашего профиля VK.
Исходный код на данный момент.
Copy link