Создание actions

Наконец-то мы подходим к вопросу взаимодействия с пользователем приложения. Практически любое действие пользователя в интерфейсе = отправка действия (dispatch actions)

По клику на кнопку года наше приложение:

  • устанавливает заголовок
  • загружает фото этого года

Сейчас предлагается рассмотреть установку заголовка. Загрузка фото требует выполнения асинхронного запроса, а чтобы добраться до этого, мы должны рассмотреть несколько интересных вещей. К тому же, установка заголовка отлично показывает на простом примере, как вращаются данные внутри redux-приложения, а именно:

  1. Приложение получило изначальное состояние (initial state)
  2. Пользователь, нажав кнопку, отправил действие (dispatch action)
  3. Соответствующий редьюсер обновил часть приложения, в согласии с тем, что узнал от действия.
  4. Приложение изменилось и теперь отражает новое состояние.
  5. ... (с пункта 2 все повторяется по кругу)

Это и есть однонаправленный поток данных.


Создадим page action:

src/actions/PageActions.js

export function setYear(year) {

  return {
    type: 'SET_YEAR',
    payload: year
  }

}

Напоминание: поля type и payload — всего лишь «негласное» соглашение. Немного об этом можно почитать на английском тут.

Поправим редьюсер page:

src/reducers/page.js

const initialState = {
  year: 2016,
  photos: []
}

export default function page(state = initialState, action) {

  switch (action.type) {
    case 'SET_YEAR':
      return { ...state, year: action.payload }

    default:
      return state;
  }

}

Обратите внимание, в аргументах у функции page указан второй аргумент — action. Это стандартные аргументы redux reducer'а. Благодаря этому мы можем легко обрабатывать различные действия по их типу, попадая в нужную секцию case оператора switch.

Также обратите внимание, что мы не изменили объект state, а вернули новый с полем year, равным action.payload (а значит годом, выбранным пользователем).


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

У нас есть action, и есть reducer, готовый изменить state приложения (да, эти слова в этой книге нарочно написаны иногда по-английски). Но наш компонент не знает как обратиться к необходимому действию.

Согласно таблице из прошлого раздела: для изменения данных наш компонент Page.js должен вызывать callback из this.props, а наш контейнер* App.js — отправлять действие (dispatch action).

* употреблено слово контейнер, хотя правильнее называть контейнером <Connect(App) />, но, пожалуй, это допустимо, так как он генерируется функцией connect на основе App.js.

Из документации функции connect, нам также становится ясно, что с помощью этой функции мы можем не только подписаться на обновления данных, но и «прокинуть» наши actions в контейнер.

Функция connect первым аргументом принимает «маппинг» (соответствие) state к props, а вторым — «маппинг» dispatch к props. Как бы дико это ни звучало, на практике это значит, что нам достаточно передать второй аргумент.

Исправим App.js

src/containers/App.js

import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import User from '../components/User'
import Page from '../components/Page'
import * as pageActions from '../actions/PageActions'

class App extends Component {
  render() {
    const { user, page } = this.props
    const { setYear } = this.props.pageActions

    return <div>
      <User name={user.name} />
      <Page photos={page.photos} year={page.year} setYear={setYear} />
    </div>
  }
}

function mapStateToProps(state) {
  return {
    user: state.user,
    page: state.page
  }
}

function mapDispatchToProps(dispatch) {
  return {
    pageActions: bindActionCreators(pageActions, dispatch)
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

Начнем с разбора mapDispatchToProps. Внутри функции мы использовали вспомогательную функцию из redux — bindActionCreators (официальная документация), которая позволила вызывать setYear, если выразиться просто с некоторыми допущениями, как:

store.dispatch({
    type: 'SET_YEAR'
    payload: 2016
})

Тем самым, необходимое изменение прослушивается в redux store, и в нашем редьюсере Page соответственно.

Следовательно, после выполнения connect(mapStateToProps, mapDispatchToProps)(App) мы получили в App.js новые свойства (props), что наглядно демонстрирует вкладка «React» в React Developer Tools. setYear in dev tools

Добавив setYear в свойства Page.js, не составит труда использовать необходимый action из компонента, который по-прежнему ничего знать не знает о redux.

UPDATE [18.03.16]: свойство innerText, приведенное в коде ниже, — нестандартное, поэтому с ним могут возникнуть проблемы в некоторых браузерах. Вместо него вы можете использовать — textContent.

src/components/Page.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export default class Page extends Component {
  onYearBtnClick(e) {
    this.props.setYear(+e.target.innerText)
  }
  render() {
    const { year, photos } = this.props
    return <div>
      <p>
        <button onClick={::this.onYearBtnClick}>2016</button>
        <button onClick={::this.onYearBtnClick}>2015</button>
        <button onClick={::this.onYearBtnClick}>2014</button>
      </p>
      <h3>{year} год</h3>
      <p>У тебя {photos.length} фото.</p>
    </div>
  }
}

Page.propTypes = {
  year: PropTypes.number.isRequired,
  photos: PropTypes.array.isRequired,
  setYear: PropTypes.func.isRequired
}

Собственно, код компонента Page по-прежнему очень простой. Строка ::this.onYearBtnClick === this.onYearBtnClick.bind(this) нужна, так как React с версии 0.14.x не привязывает this к компоненту.

Использование двойного двоеточия — это возможность ES7 (experimental), которая доступна в babel с настройкой stage=0 (для тех, кто писал код, начиная с раздела «Подготовка» — все уже настроено, смотри файл .babelrc)


Глава выдалась достаточно длинной, а хуже всего, что мы написали «кипу» кода всего лишь для обновления цифры в заголовке. Где профит, как говорится?

Профит обнаружится дальше, когда ваше приложение разрастется. Когда его будет необходимо поддерживать и добавлять новые фичи. За счет однонаправленного потока данных (юзер кликнул — действие вызвалось — редьюсер изменил состояние — компонент отрисовал изменения) даже в приложении, написанном давно, у вас получится очень быстро разобраться и внести необходимые обновления, которые требует бизнес. К тому же, такой подход отлично работает и для командной работы.


Исходный код

results matching ""

    No results matching ""