
こんにちは、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に公開しています。