【React】ReduxのState/Store/Action/Reducer設定まとめ

Vue.jsを使用している際にもVuexやVue Routerなどで、
Stateを全てのコンポーネントに渡すことができるという機能がありましたが、
そのReact版がReact Reduxです。

大まかに、React Reduxを使ったState/Store/Actionの設定まとめをしていきます。

React Reduxの必要性

Reactを使って多重階層のコンポーネントを用いる際、Storeという保管庫からどのコンポーネントからでも簡単にデータを取り出すことができるため便利。

従来のState管理では渡したいデータがあった場合、親コンポーネントから子コンポーネントへ、そして子コンポーネントから孫コンポーネントへ…という煩雑な手間がありました。

下準備

Reactプロジェクト内で、reduxとreact-reduxという2つのパッケージをインストールする。

$ yarn add redux react-redux

また以下のような階層でState/Store/Actionを区分けしておくと便利
📦src
┣ 📂actions
┃ ┗ 📜index.js
┣ 📂components
┃ ┗ 📜App.js
┣ 📂reducers
┃ ┣ 📜count.js
┃ ┗ 📜index.js
┣ 📜index.css
┣ 📜index.js
┗ 📜serviceWorker.js

Action

全てのコンポーネントで共通したアクションを登録しておくためのファイル。
またActionのファイル内で、アクションを定義する関数のことをAction Createrという。

以下は一例
src/actions/index.js

// 複数箇所でINCREMENTなどは使用されるため変数化しておく
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'INCREMENT'

// 下記のINCREMENTとDECREMENTでは書き方が違うが、
// 上はリファクタリングが行われているだけで意味は同じ
// またactionを定義してreturnする関数をアクションクリエイターという
export const increment = () => ({
        type: INCREMENT
})

export const decrement = () => {
    return {
        type: DECREMENT
    }
}

Reducer

アクションが発生した際に、そのアクションに組み込まれているタイプによってどのようにStateを変化させるのかを設定しておくのがReducerです。

全てのReducerをまとめるためのsrc/reducers/index.jsと、個別のReducerであるsrc/reducers/count.jsの2つを用意しました。

src/reducers/index.js

// 全てのReducerをコンバイン(合体)させるためのもの
import { combineReducers } from 'redux'
import count from './count'

export default combineReducers({ count })

実際のReducerの役割は以下のように個別のReducerファイルに分ける
src/reducers/count.js

import { INCREMENT, DECREMENT } from '../actions'

const initialState = { value: 0 }

export default (state = initialState, action) => {
    // eslint-disable-next-line default-case
    switch(action.type) {
        case INCREMENT:
            return { value: state.value + 1 }
        case DECREMENT:
            return { value: state.value - 1 }
        default:
            return state
    }
}

Store

Storeは、アプリ内にある全てのStateを管理するための保管庫というイメージです。
Storeを設定することによってどのコンポーネントからでも値の受け渡しをせずにStateを取り出すことができるようになります。

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// 以下2つをインポート
import { createStore } from 'redux';
import { Provider } from 'react-redux';

import './index.css';
// 上記で作成したReducerをインポート
import reducer from './reducers';

import App from './components/App';
import * as serviceWorker from './serviceWorker';

// アプリ内唯一のstoreを定義。
// またreducerを引数に入れることで
// 全てのアプリケーションのStateがStoreに格納される。
// reducerのcount.js内にあるinitialState = { value: 0 }が渡される。
const store = createStore(reducer);

ReactDOM.render(
 // Providerタグで囲い、store属性に定義したstoreを格納することで、
   // 全てのコンポーネントからStore内に格納されたStateを参照できるようになる
    <Provider store={store}>
    <App />
    </Provider>,
    document.getElementById('root')
    );

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

StateとActionを関連づけるConnect

Stateを管理するためのStore、Actionを設定するためのActionとAction Creater、
そして操作によって適切なActionを呼び出すためのReducersを設定しました。

最後にActionによって適切にStateに処理が反映させるためにconnectを導入します。
src/components/App.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

import { increment, decrement } from '../actions';

class App extends Component {
  render() {
    // stateやactionを格納するためのprops
    const props = this.props

    return (
    <div>
    {/* reducerのcount.js内にあるvalue(initialState)を表示する */}
    { props.value }
    <button onClick={props.increment}>+1</button>
    <button onClick={props.decrement}>-1</button>
    </div>)
  }
}
// mapStateToPropsはStateの情報からコンポーネントで必要な情報を取り出して、
// コンポーネント内のpropsとしてマッピングする機能を持つ
// 引数にはstateを書いて、どういったオブジェクトをpropsとして対応させるのかを定義させる
const mapStateToProps = state => ({ value: state.count.value })

// mapDispatchToPropsはあるアクションが発生した時にReducerにTypeに応じた状態遷移を実行させるための関数
// incrementのためのボタンとdecrementのためのボタンに必要なアクションをdispatch関数の引数に渡す
const mapDispatchToProps = dispatch => ({ 
  increment: () => dispatch(increment()),
  decrement: () => dispatch(decrement())
})

export default connect(mapStateToProps, mapDispatchToProps)(App);

これでどのコンポーネントでも同じAction、Stateを使用できるようになりました。

ちなみにmapDispatchToPropsには省略記法があり、以下のように書くことができます。

const mapDispatchToProps = ({ increment, decrement })

たった1行だけですが、処理的に違いはありません。