Table of Contents
ReactとReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果は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ボタンをクリックすると、以下の流れで状態が遷移する。
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を追加して、非同期処理を実装する。