フロントエンド開発Blog

オレには鈍器がある

ES2015 , JavaScript , Material-ui , React , Redux , babel , webpack

以前、LAMP環境で動作するcakePHP製の家計簿アプリのフロントエンドを刷新しました。(以前の記事:WEB上で出費を管理「ふたりの家計簿」を公開しました

掲題の通りReact + Redux + Material-UIを組み合わせてみました。コンパイルにはWebpack, Babel、文法チェックはESLintを使用しました。

コンパイル環境にはtakanabe様の「Material UIを使ってカッコいいUIのReactアプリケーションを作ってみた」をベースにしました。1から自分で作っていたらもっと大変だったと思います。この場を借りて謝辞とさせていただきます。ありがとうございます!

少し解説

Reduxということで

  1. action creatorがactionを発行
  2. reducerがactionから新しいstateを発行し、containerに渡す
  3. containerから各componentにpropsとしてstate情報を配給

という基本型を意識して作りました。かいつまんで説明していきます。

エントリーポイント

src/index.jsx

webpackのエントリーポイントです。reactのマウント、routerの設定、初回アクセス時に発行するアクションの設定を行っています。

ページにアクセスしたばかりの状態だとstateは空の状態なので、AJAXで現在年月データを取得し、stateに反映する必要があります。

import { fetchMonthlyData } from '../actions/monthly';
const pageHistory = syncHistoryWithStore(browserHistory, store);
pageHistory.listen(function(location){
  // 月別データ表示
  if(location.pathname.match(/monthly/)){
    store.dispatch(fetchMonthlyData({start: location.pathname.slice(-6)}));
  }
  // インデックスに戻ってきた際、PHP側でtplに吐き出された現在年月から該当月データを取得
  if(location.pathname === '/kakeibo/forms/index'){
    store.dispatch(fetchMonthlyData({start: startYear + startMonth}));
  }
});

このように、/kakeibo/forms/indexというトップページにアクセスしたら月データ取得AJAXを走らせるようにしています。

このlistenというのが肝になりますね。特定のURLパターンにマッチしたら最短で実行したい処理をここに書きます。

今回は月別アーカイブ、トップの現在月データ取得の2つのパターンでlistenを用意しています。

エントリーポイント配下に複数のコンポーネントがぶら下がっている形になりますが、全てを説明しているときりがないのでTOPでも使っている月間データ取得部分に焦点を絞って解説します。

action

actions/monthly.jsx

(これがベストな方法かはさておき)非同期処理はActionで書く方針にしました。取得中、取得成功、取得失敗の3パターンのアクションクリエイターを用意し、reducerにactionを渡します。

/**
 * 月別データ取得AJAXの通信アクション
 * @return {Object} 削除通信中アクション->通信完了アクション
 */
export function fetchMonthlyData(data) {
  let params = new URLSearchParams();
  let i;

  // AJAXパラメータオブジェクトを生成
  for(i in data){
    params.append(i, data[i]);
  }

  return dispatch => {
    // 通信中アクションを実行
    dispatch(fetchMonthlyDataRequest(data));

    // AJAX
    Axios.post(
      ajaxUrl.getMonthData,
      params,
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
    ).then(
      response => dispatch(fetchMonthlyDataResult(response.data))
    ).catch(
      () => dispatch(fetchMonthlyDataResult({status: "0", msg: "月別データの取得に失敗しました。通信状況を確認してください"}))
    )
  }
}

exportするのはこの関数だけです。AxiosというAJAXライブラリを使用しています。Axios自体の使い勝手は賛否あるようですが、単純にPOSTするだけなら問題は・・・ひとつだけありました。

headersを指定しないとpayloadとして送信するため、jQuery感覚で送信してもだめでした。PHP側での値の取得部分に手を加える必要がでてしまい、今回のガワ変更という趣旨から外れます。(PHPに手を入れるのは最低限にしたかった)

そこでheadersにContent-Typeを指定したのですが、そうすると今度は送信データをObject作って渡してもString型になってしまいました。URLSearchParamsを使うと解決できるようですがmobileブラウザでまったく動きません。

最後まで悩みましたがpolyfillを使わせていただきました。

これであっさり解決しちゃいました!

monthly以外にもAJAXを絡めたaction creatorはありますが、どれも構造自体は一緒です(del.jsx、edit.jsx、year.jsx)

reducer

reducers/monthly.jsx

超完結です。action.typeに応じて決まったstateを返すだけです。

FETCHED_POSTS_SUCCESSとFETCHED_POSTS_FAILUREにはAJAXで取得したdataをぶら下げてstateとして発行しています。reducerの処理はシンプルなのでコードは割愛します。

Container

containers/App.jsx

コンポーネントを包んでいるコンテナです。routerなどで直接マウントされる対象、ともいえます。

function mapStateToProps(state, ownProps) {
  return {
    monthly: state.monthly,
    drawmenu: state.drawmenu,
    del: state.del
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actionsMonthly: bindActionCreators(Monthly, dispatch),
    actionsDrawMenu: bindActionCreators(DrawMenu, dispatch),
    actionsDel: bindActionCreators(Del, dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

先ほどreducerで発行したstateは各コンポーネントに渡す必要があります。mapStateToPropsでオブジェクト形式でstateをpropsに変換して返します。こうすることでAppコンテナ内ではstateをthis.propsの形で参照できるようになります。

action creatorも同様に、コンポーネントに渡すことが可能です。mapDispatchToPropsの部分が該当部分になります。

reduxのデータの流れは以上になります。コンポーネント以下はviewの機能を担うreactがメインになります。コンポーネント全てを解説していると長大になるので割愛しますが、基本的には上記のconnectで割り当てたstateを使ってrenderを制御し、ユーザー操作を受けたらaction creator経由でstateを更新する流れになります。

Container

components/DetailTable.jsx

DetailTableを例にしてみてみると、renderの中で先ほどconnectで割り当てたstateをprops経由で表示制御に使用しています。また、ボタンイベントにはaction creatorを叩く処理が割り当てられています。

基本的なComponentの作りは皆一緒です。

実際に使ってみて

実は今回reactもreduxもmaterial-uiも初めてでした。とっつきやすくはありませんが、action、reducer、container、componentのように役割がしっかり分かれているため、コードを読むときに便利だなーと思います。特に人のソースコードを見るときはいいかもしれません。お互いが同じルールブックを元にコードを書いたり読んだりできるから。

reduxやreact自身のパワーもさることながら、統一化・画一化された規約という意味でもreduxやreactは有益かもしれません。

ページトップへ

関連ページ

ページトップへ