こんにちは、mabuiです。
前回の記事で取り上げた、Reactのみで実装したサンプルに
Reduxを導入していきます。
動作は前回とまったく同じで、仮想通貨の上位10銘柄を表示して、
名前、金額、時価総額、出来高でソートできる機能と、金額表示を切り替える機能をつけています。
Reduxを使用すると、ReactのコンポーネントそれぞれでsetStateメソッドを
使用して実行していたstateの状態管理を、
storeという役割の場所で一元管理できるため、
見通しのいいプログラムが書けるようになります。
redux, react-reduxのインストール
まずは使用するライブラリをインストールします。
コマンドラインで下記を実行します。
1 2 3 |
npm install --save redux npm install --save react-redux |
役割ごとのファイル作成
ファイル一枚でも完結できますが、役割ごとにディレクトリ、
ファイルを分割します。
ざっくり処理の流れは
container → component → actions → reducers です。
containerではcomponentの前処理を担当します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const mapStateToProps = (state) => { return state } const mapDispatchToProps = (dispatch) => { return { // ActionCreatorの関数をdispatchせずに実行できる ...bindActionCreators(ReduxTestActions, dispatch) } } export default connect(mapStateToProps, mapDispatchToProps)(ReduxTest) |
mapStateToPropsメソッドではstateの情報をpropsに渡して、
componentの好きな場所で状態を参照できるようにします。
mapDispatchToPropsメソッドではactionsをdispatchメソッドに渡して、
reducersに送ります。
bindActionCreatorsメソッドを使用して、actionsをまとめて実行しており、
actionsのメソッド内でdispatchしています。
componentは前回までの記事でも登場したReactのコンポーネントで、
JSXのタグをレンダリングする処理を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import React from 'react'; import Constants from '../constants'; // コンポーネント作成 class ReduxTest extends React.Component { convertJsx() { const display = this.props.display; if (typeof this.props.list !== 'undefined') { // JSXに変換 return this.props.list.map((coin) => <li>{coin.rank}:{coin.name}, [price: {display === Constants.DISPLAY_JPY ? coin.price_jpy : coin.price_usd}], [market_cap: {display === Constants.DISPLAY_JPY ? coin.market_cap_jpy : coin.market_cap_usd}], [percent_change_24h: {coin.percent_change_24h}]</li> ) } } createHeader() { const name = <button onClick={() => this.props.sortName()}>name</button> const price = <button onClick={() => this.props.sortPrice()}>price</button> const marketCap = <button onClick={() => this.props.sortMarketCap()}>market_cap</button> const percentChange24h = <button onClick={() => this.props.sortPercentChange24h()}>percent_change_24h</button> const display_btn = <button onClick={() => this.props.changeDisplay()}>{this.props.display}</button> return <li>#:{name}, {price}, {marketCap}, {percentChange24h}, {display_btn}</li> } constructor(props) { super(props); props.setCoins(); } render() { return ( <ul> {this.createHeader()} {this.convertJsx()} </ul> ) } } export default ReduxTest |
前回までのロジックではヘッダー情報を同じstateに混ぜ込んでいましたが、
分離しました。
createHeader メソッドを見るとわかるように、
mapDispatchToPropsメソッドに渡したactionsのメソッドは、
this.propsから自由に呼び出せるようになっています。
actionsではビジネスロジックを記述して、dispatchメソッド経由で
reducersに結果を渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
import fetch from 'isomorphic-fetch'; import ActionType from './action_type'; import Constants from '../constants'; function returnCoins(list) { return { type: ActionType.SET_COINS, list: list, } } function sortCondition(list, condition) { // 同じオブジェクトをreducerに渡してもstateが更新されず、レンダリングが走らないので、参照渡しで更新用のオブジェクト生成 const newList = list.slice(); // ソート newList.sort((a, b) => { return a[condition] < b[condition] ? -1 : 1; }); return { type: ActionType.SORT_CONDITION, list: newList, } } const Actions = { sortName() { const list = this.list; // redux-thunkを使用しているので、関数を返却しないとエラーになる return function (dispatch) { dispatch(sortCondition(list, 'name')); } }, sortPrice() { const list = this.list; return function (dispatch) { dispatch(sortCondition(list, 'price')); } }, sortMarketCap() { const list = this.list; return function (dispatch) { dispatch(sortCondition(list, 'market_cap')); } }, sortPercentChange24h() { const list = this.list; return function (dispatch) { dispatch(sortCondition(list, 'percent_change_24h')); } }, changeDisplay() { // stateのdisplayをUSD ⇔ JPYに変換して、ヘッダー、金額の表示も更新 const display = this.display === Constants.DISPLAY_JPY ? Constants.DISPLAY_USD : Constants.DISPLAY_JPY return { type: ActionType.CHANGE_DISPLAY, display: display, } }, setCoins() { // redux-thunkを使う場合、非同期通信処理は関数として返す必要がある。 // storeのdispatchを引数として取得できるので、それを使って返り値をreducerに渡す。 return function (dispatch) { // coinmarketcapから上位10位の銘柄取得api let endpoint = "https://api.coinmarketcap.com/v2/ticker/?convert=JPY&limit=10&sort=rank" // fetchでリクエストを投げる。非同期処理でPromiseのオブジェクトが返却される。 let dict = {}; fetch(endpoint).then((response) => { let json = response.json(); json.then((value) => { let data = value.data; for (let key in data) { let coin = data[key]; let quotes_jpy = coin['quotes']['JPY']; let quotes_usd = coin['quotes']['USD']; // レスポンスを連想配列に詰める。 dict[coin.rank] = { rank: coin['rank'], name: coin['name'], price_jpy: quotes_jpy['price'], market_cap_jpy: quotes_jpy['market_cap'], price_usd: quotes_usd['price'], market_cap_usd: quotes_usd['market_cap'], percent_change_24h: quotes_usd['percent_change_24h'], } } }) }, (error) => { console.error(error); }).then(() => { // setTimeoutで非同期処理の結果(dict)が帰ってくるのを待つ。 setTimeout(() => { let list = []; // rank順に並び替え for (let rank in dict) { list.push(dict[rank]); } dispatch(returnCoins(list)); }, 100) }) } }, } export default Actions |
非同期処理を行っているため、redux-thunkを使用していて、
そのためactionsのメソッドの返り値はオブジェクトではなく
全て関数になっています。
bindActionCreatorsメソッドからdispatchメソッドが渡ってきているため、
メソッド内で使用できます。
このへんを理解するのに時間がかかりましたが、
redux-thunkを使用するとactionsは返り値をメソッドにできるかつ、
dispatchメソッドの引数にメソッドを与えて実行できるようになるため、
その処理内の好きなタイミングでdispatchメソッドを使えるようになります。
そのため、非同期処理内でもdispatchが実行できるようになるのです。
Actions.setCoins()では、fetch、さらにはsetTimeoutの内部でdispatchを
実行して非同期処理の結果をreducersに送っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// fetchも非同期処理 fetch(endpoint).then((response) => { ... }).then(() => { // setTimeoutで非同期処理の結果(dict)が帰ってくるのを待つ。 setTimeout(() => { let list = []; // rank順に並び替え for (let rank in dict) { list.push(dict[rank]); } dispatch(returnCoins(list)); }, 100) |
reducersはactionsからの結果を元に、storeに情報を送り、
stateの状態を更新します。
更新が発生すると再度レンダリングが走り、画面が更新されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import ActionType from "../actions/action_type"; import Constants from '../constants'; const initialState = { display: Constants.DISPLAY_JPY, list: [] } function reducer(state = initialState, action) { switch (action.type) { case ActionType.SET_COINS: { return { ...state, list: action.list, } } case ActionType.SORT_CONDITION: { return { ...state, list: action.list, } } case ActionType.CHANGE_DISPLAY: { return { ...state, display: action.display, } } default: // フォールバック処理 return state } } export default reducer; |
まとめ
Reduxを導入することでコンポーネントがすっきりしました。
またsetStateメソッドを使わないことで、状態の更新処理が隠されて、
コードの見通しが良くなったかと思います。
今回作成したコードはgithubに公開しています。