ReactReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。)

前回はReduxをセットアップした。

(2018/11/21更新)

React Redux

前回はReduxをセットアップして、ActionをStoreにディスパッチしてstateを更新できるようになった。 今回はこれをReactにつなぐ。

使うのはReact Redux

$ yarn add react-redux

v5.1.1が入った。

Presentational Components と Container Components

React Reduxの使い方を理解するには、Presentational Components と Container Components という概念を知らないといけない。 これはReactコンポーネントを役割別に分ける考え方で、それぞれ以下のような特徴をもつ。

Presentational Components Container Components
主な役割 DOMをレンダリングする データを取得したりstateを更新したりする(Reduxとつなぐ)
Reduxとの関連 無し 有り
データの読み込み propsから読む Reduxのstateオブジェクトから読む
データの更新 propsで渡されたコールバックを呼ぶ ReduxのActionをディスパッチする
作り方 自前で書く React Reduxで生成する


要するに、普通にReactで作ったUIコンポーネントを、React Reduxで生成するContainer ComponentでラップしてやることでReduxのStoreとつなぐことができる。

connect()

Container Componentの生成にはReact Reduxのconnect()というAPIを使う。

React Reduxを使う場合、Reduxのstateの更新に応じてReactコンポーネントに新しいpropsを渡して再レンダリングすることになるが、この新しいpropsを作ってコンポーネントに渡す処理を定義するのがconnect()

connect()の第一引数には、ReduxのstateのプロパティとReactコンポーネントのpropsのプロパティとのマッピングをする関数であるmapStateToProps()を渡す。 mapStateToProps()はstateの更新に応じて呼び出され、引数にstate(と現在のprops)が渡される。 mapStateToProps()が返すオブジェクトはReactコンポーネントに渡されるpropsにマージされる。

connect()の第二引数には、Storeのdispatch()を呼び出す処理とReactコンポーネントのpropsのプロパティとのマッピングをする関数であるmapDispatchToProps()を渡す。 mapDispatchToProps()の引数にはdispatch()が渡される。 mapDispatchToProps()が返すオブジェクトはReactコンポーネントに渡されるpropsにマージされる。

(mapDispatchToProps()は第二引数にpropsを受け取ることもできて、この場合、propsの更新に反応して呼び出されるコールバックになる。)

connect()を実行すると関数が返ってくる。 この関数にReactコンポーネント(Presentational Component)を渡して実行すると、Storeに接続されたReactコンポーネント(Container Component)が返ってくる。

connect()の使い方

前回作ったStoreをHOGEボタン(これはPresentational Component)につなげるContainer Componentを書いてみる。 Container Componentのソースはsrc/containers/に入れる。

src/containers/HogeButton.jsx

import React from 'react';
import Button from '@material-ui/core/Button';
import { connect } from 'react-redux';
import { hogeButtonClicked } from '../actions/actions';

function mapStateToProps(state) {
  return {
    clicked: state.hoge.clicked
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onClick: function() {
      dispatch(hogeButtonClicked());
    }
  };
}

const HogeButton = connect(
  mapStateToProps,
  mapDispatchToProps,
)(Button);

export default HogeButton;

こんな感じ。

HOGEボタンをクリックすると、以下の流れで状態が遷移する。

  1. hogeButtonClicked()が呼ばれてHOGE_BUTTON_CLICKEDアクションが生成されてdispatchされる。
  2. Storeの中でstate.hoge.clickedが更新される。
  3. stateの更新に反応してmapStateToProps()が呼び出され、その戻り値がpropsにマージされる。
  4. 新しいpropsを使って、新たにHOGEボタンがレンダリングされる。

connect()のシンプルな書き方

mapDispatchToPropsは実はプレーンオブジェクトでもいい。 この場合、オブジェクトのキーと値はそれぞれ、propsのプロパティ名とAction Creatorにする。 (Action Creatorはconnect()dispatch()でラップしてくれる。)

const mapDispatchToProps  {
  onClick: hogeButtonClicked,
};


また、mapStateToPropsmapDispatchToPropsはexportするわけでも再利用するわけでもないので、connect()の中に直接書いてしまってもいい。 この場合、mapStateToPropsはアロー関数で書いて、returnは省略してしまうのがいい。

const HogeButton = connect(
  (state) => ({
    clicked: state.hoge.clicked
  }),
  {
    onClick: hogeButtonClicked,
  },
)(Button);


さらに、mapStateToPropsが受け取るstateは、hogeプロパティしか興味ないので、オブジェクト分割代入をするのがいい。

const HogeButton = connect(
  ({hoge}) => ({
    clicked: hoge.clicked
  }),
  {
    onClick: hogeButtonClicked,
  },
)(Button);


まとめると、以下のように書けるということ。

src/containers/HogeButton.jsx

import React from 'react';
import Button from '@material-ui/core/Button';
import { connect } from 'react-redux';
import { hogeButtonClicked } from '../actions/actions';

const HogeButton = connect(
  ({hoge}) => ({
    clicked: hoge.clicked
  }),
  {
    onClick: hogeButtonClicked,
  },
)(Button);

export default HogeButton;


参考: シンプルなreact-reduxのconnectの書き方

reselect

mapStateToPropsはstateが更新されるたびに呼ばれるので、中で複雑な計算してたりするとアプリ全体のパフォーマンスに影響を与える。

このような問題に対応するため、stateの特定のサブツリーが更新された時だけmapStateToPropsの先の計算を実行できるようにするライブラリがある。 それがrelesect

reselectは重要なライブラリだとは思うけど、とりあえずほって先に進む。

HogeButtonのアプリへの組み込み

作ったHogeButtonは、普通のコンポーネントと同じように使える。

src/components/App.jsx:

 import React from 'react';
 import styled from 'styled-components';
-import Button from '@material-ui/core/Button';
+import HogeButton from '../containers/HogeButton';
 import Fonts from '../fonts';

 const Wrapper = styled.div`
   font-size: 5rem;
 `;

 const App = () => (
   <Wrapper>
-    <Button variant="contained">
+    <HogeButton variant="contained">
       HOGE
-    </Button>
+    </HogeButton>
     <Fonts />
   </Wrapper>
 );

 export default App;

Provider

全てのContainer ComponentsがReduxのStoreの変更をサブスクライブする必要があるので、それらにStoreを渡してやらないといけない

Storeをpropsに渡して、子コンポーネントにバケツリレーさせたりして行きわたらせることも可能だけど面倒すぎる。 ので、React Reduxがもっと簡単にやる仕組みを提供してくれている。 それがProviderというコンポーネント。

Providerの子コンポーネントはStoreにアクセスしてconnect()を使えるようになる。 ざっくり全体をProviderで囲ってやるのがいい。

src/index.jsx:

 import React from 'react';
 import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
 import App from './components/App';
+import configureStore from './configureStore';

+const store = configureStore();
 const root = document.getElementById('root');

 if (root) {
   ReactDOM.render(
-    <App />,
+    <Provider store={store}>
+      <App />
+    </Provider>,
     root,
   );
 }


次回は、ReduxにMiddlewareを追加して、非同期処理を実装する。