Redux 入門

Redux とは、React Component から state を変更するためのロジックを排除し、別クラスによって管理するためのフレームワークです。よって、setState は一切書きません。Redux では state の変化に合わせて props を都度変えるようにするので、render 時に参照する値は props の方になります。

まずは Redux の一覧の流れを確認してみましょう。

Component (⇆ Action) → Reducer → Component

Component で state を変更するアクションが起きたら、アクション内容を定義した関数(Action
にあたる部分)にアクション情報を渡して特定の形式のオブジェクトを取得し、それを Reducer に渡します。そして Reducer は、渡されたアクション情報から新しい state を生成して、最後にそれをComponent が受け取ってレンダリングする、という流れになっています。

Reducer とは何か、を言葉で説明するよりはソースを見たほうが早いので、まずは Redux を使用していないページを用意して、それを Redux 化していく作業をしていきます。用意するのはテキストフィールドが1つだけ表示されるページです。

webpack の利用を想定しています。React の開発環境は React.js 入門 をご参照ください。
sample_app
    ├  package.json
    ├  webpack.config.js
    ├  index.html
    ├  index.js
    └  inputView.js

index.html

package.json

{
  "scripts": {
    "build": "webpack",
    "dev": "webpack-dev-server --hot --inline"
  },
  "devDependencies": {
    "babel-core": "^6.25.0",
    "babel-loader": "^7.1.1",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "webpack": "^3.4.1",
    "webpack-dev-server": "^2.5.1"
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}

webpack.config.js

const path = require('path')
const publidDir = path.join(__dirname)

module.exports = [
  {
    entry: [
      './index.js'
    ],
    output: {
      path: publidDir,
      publicPath: '/',
      filename: 'bundle.js'
    },
    module: {
      loaders: [{
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
          presets: ['react', 'es2015']
        },
      }],
    },
    resolve: {
      extensions: ['.js']
    },
    devServer: {
      contentBase: publidDir
    },
  }
]

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import InputView from './inputView'

ReactDOM.render(<InputView />, document.getElementById('root'))

inputView.js

import React from 'react'

export default class InputView extends React.Component {
  constructor(props) {
    super(props)
    this.state = {text: 'default'}
  }

  changeValue(text) {
    this.setState({text: text})
  }

  render () {
    return (
      <input
        value={this.state.text}
        onChange={(e) =>  {
          e.preventDefault()
          this.changeValue(e.target.value)
        }}
      />
    )
  }
}

webpack-dev-server を起動して、localhost:8080 にアクセスすると、default と入力されたテキストフィールドが表示されると思います。では、このページに Redux を導入していきます。

インストール

以下のコマンドを実行します。

$ npm install --save redux react-redux

Redux の設定

まず、state を変更するイベントの changeValue() はアクションにあたるので、外部の関数に定義します。actions.js というファイルを作成して以下のようにしてください。

export const ActionType = {
  CHANGE_VALUE: 'CHANGE_VALUE'
}

export const changeValue = (text) => {
  return { type: ActionType.CHANGE_VALUE, text: text }
}

ここではアクション名を文字列の定数として定義したり、changeValue が呼ばれたら渡された情報をもとにオブジェクトを返すようにしています。これが後で Reducer に渡す情報になります。重要なのは typeというキー名を含めることで、あとのキーは自由に設定できます。規模が大きいアプリケーションではアクションを複数のコンポーネントから参照するので、アクション名は被らないようにしましょう。

次に、コンポーネントの state を管理する Reducer を作成します。reducers.js というファイルを作成し、以下のようにしてください。

import { ActionType } from './actions.js'

export default (state = { text: 'default' }, action) => {
  switch (action.type) {
    case ActionType.CHANGE_VALUE:
      return Object.assign({}, state, { text: action.text })
      break
    default:
      return state
  }
}

第1引数でデフォルトの state, 第2引数でアクションの内容を受け取り、新しい state となるオブジェクトを返しています。Reducer の役割は、どういう状態の時にどういうアクションがあれば、必ずこうなる、というのを保証することです。どのコンポーネントから呼ばれても、アクション内容が同じであれば必ず同じ結果(=state)を返します。Object.assign を使っているのは、Reducer で引数の state を直接いじってはいけない、という Redux のルールがあるからです。もし state を直接いじってしまうと正常に動作しなくなくなります。

では、コンポーネントから Reducer にアクションを送ってみましょう。アクションを送るには、Redux で提供されている Store オブジェクトの dispatch メソッドを使用します。

Store オブジェクトは親コンポーネントで生成して、子・子孫コンポーネントへ props で渡す必要があるのですが、それを簡潔にかける Provider タグを使用します。Store オブジェクトの生成には createStore 関数を使用します。では、親コンポーネントである index.js を以下のようにしましょう。

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import InputView from './inputView'
import Reducers from './reducers'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={createStore(Reducers)}>
    <InputView />
  </Provider>
  ,
  document.getElementById('root')
)

Provider タグに store を渡すことで、子・子孫コンポーネントの props に自動的に Store がセットされます。Store の中身は4つの関数(dispatch, subscribe, getState, replaceReducer)です。Store の dispatch 関数を呼ぶと、生成時に紐付けた Reducer までイベントが伝達される仕組みになっています。

次に、イベントを発行する inputView.js に移りましょう。以下のソースをコピペしてください。

inputView.js

import React from 'react'
import { connect } from 'react-redux'
import { changeValue } from './actions.js'

class InputView extends React.Component {
  render () {
    const { text, changeValue } = this.props
    return (
      <input
        value={text}
        onChange={(e) =>  {
          e.preventDefault()
          changeValue(e.target.value)
        }}
      />
    )
  }
}

const mapStateToProps = (state) => {
  return {text: state.text}
}

const mapDispatchToProps = (dispatch) => ({
    changeValue: (text) => dispatch(changeValue(text))
  }
)

export default connect(mapStateToProps, mapDispatchToProps)(InputView)

まず、dispatch をするコンポーネントは、react-redux で提供されている connect 関数を使って Store と結びつけます。connect 関数の返り値は関数になっていて、その引数にコンポーネントを渡すと Store と結びついた状態のコンポーネントが返ってくるので、それを export するようにしています。

ここで、引数に mapStateToPropsmapDispatchToProps という2つの関数を渡しており、それぞれの返り値がコンポーネントの props に自動的に設定される仕組みになっています。

mapStateToProps 関数は、Reducer から受け取った state 情報を自身の props に設定するための関数で、mapDispatchToProps 関数は引数に dispatch 関数が入ってくるので、それを使って Reducer にアクションを伝達するための関数を定義したものです。ちなみに同名でわかりにくいですが、dispatch の引数の changeValue は actions.js から読み込んだ関数です。

今度はテキストフィールドの方に移ります。onChange で props に入っている(mapDispatchToProps の) changeValue 関数を呼び出して、{type: ‘CHANGE_VALUE’, text: “引数の value”} という形式で、アクション情報とテキストの入力情報を Reducer に渡しています。Reducer の方ではこれが 第2引数 に入ってくる情報になります。

そして最後に、Reducer から渡された state を mapStateToProps 関数で受け取って、自身の props にセットしたあと、自動的に render が走る、という流れになっています。

これで Redux を使用した React コンポーネントが作成できました。あとは公式サイトを見ながら、実際の現場で必要なことを随時調べるようにしましょう。