Table of Contents
ReactとReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。)
前回はReduxをセットアップした。
(2018/11/21更新)
React Redux
前回はReduxをセットアップして、ActionをStoreにディスパッチしてstateを更新できるようになった。 今回はこれをReactにつなぐ。
使うのはReact Redux。
$ yarn add react-reduxv5.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ボタンをクリックすると、以下の流れで状態が遷移する。
hogeButtonClicked()が呼ばれてHOGE_BUTTON_CLICKEDアクションが生成されてdispatchされる。- Storeの中で
state.hoge.clickedが更新される。 - stateの更新に反応して
mapStateToProps()が呼び出され、その戻り値がpropsにマージされる。 - 新しいpropsを使って、新たにHOGEボタンがレンダリングされる。
connect()のシンプルな書き方
mapDispatchToPropsは実はプレーンオブジェクトでもいい。
この場合、オブジェクトのキーと値はそれぞれ、propsのプロパティ名とAction Creatorにする。
(Action Creatorはconnect()がdispatch()でラップしてくれる。)
const mapDispatchToProps ⁼ {
onClick: hogeButtonClicked,
};
また、mapStateToPropsとmapDispatchToPropsは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を追加して、非同期処理を実装する。