積雲が映像制作したMV『RANGEFINDER』公開中
専門88IO

【React+Redux】Reducerの処理が汚くなる人へ

専門

Reactを用いるに当たって、stateは直接変更してはいけないというルールがありますよね。

以下はReactでのClass ComponentとFunctional Componentのstate管理方法の一例です。

また、Functional ComponentでもReact.useEffect()でLifecycle Methodを作ることができるので、筆者個人としてはFunctional Componentで統一しても問題ないのかなといった所見です。

さて、このstate管理(保存・変更)をReduxを導入してReducerで定義すると以下のようになります。(Actionが必要となりますが割愛します。)

本題として、このstateの返し方を考えてみます。

概略

  • Reducerにおける処理は非破壊的メソッドを用いてシンプルにまとめる
  • Thunk Middlewareを導入した場合、ActionCreatorで複雑な処理を記述してもよい

Reducerで複雑な処理は行わない

以下のようなコードは御法度ではないでしょうか。(上のコードを一部変更します)

case "GET_DATA":
  let res = axios.get("/data");
  if (!res.data)  break;
  return {
    data: res.data
  };

非同期処理が…とかは言わないでください。お願いします。

これはリクエスト処理をどこに記述するか分からなくなってReducerに書いてしまったパターンです。

その気持ち、すごく分かります。だから書きました。

正しくは「ActionCreatorで定義する」です。

ActionCreatorは以下のようなシンプルなオブジェクトじゃないといけないって書いてあったんだけど…

const getData = (data) => ({
  type: "SET_DATA",
  payload: {
    data
  }
});

と、覚えたかもしれませんがReduxのMiddlewareであるredux-thunkを導入すれば解決します。

このMiddlewareは非同期処理の実装、Action Creatorの拡張を可能にします。(導入方法は上のリンクからREADMEを読んでください。)

書き換えたコードは以下のようになります。

const getDataSuccess = (data) =-> ({
  type: "GET_DATA_SUCCESS",
  payload: {
    data 
  } 
});

const getData =() => {
  return (dispatch) => {
    axios.get("/data")
      .then(res => getDataSuccess(res.data);
  };
};

公式のサンプルではこれに加えて

  • リクエストをとばす前の getDataRequest()
  • データ取得に失敗したときの getDataFailure()

といった2つのActionCreatorを定義していた気がします。

この方法では、Actionを実行するためのActionCreatorはシンプルな状態に保ち、それを実行するための関数を定義しています。(他のActionCreatorと同様にdispatch(getData())で着火できます。)

こうすることで、Reducerでの処理をシンプルにまとめることができます。

case "GET_DATA_SUCCESS":
  return {
    ...state,
    data: action.payload.data
  };

余談ですが、ActionCreatorをreturnで返すか否かは宗教の問題だと思います。違ったら教えてください。

Stateが配列や連想配列だった場合

前述の通りstateは直接変更してはいけないので、配列型のstateに破壊的メソッド(push, splice, pop等)は使えません。別の変数にdeep copyし、変更を加えてから値を返すといった手法も度々見かけますが(同期の読んでいた本がそうでした)、筆者は可読性が低くなっているように感じ好みません。

そこで、非破壊的メソッドを用いてワンライナーで簡潔に書ける方法をまとめます。

以下、stateをdata, 配列型変数をarray, 連想配列型変数をdictとして、Reducerの戻り値の一部として記述します。

値から配列, 連想配列への変換

連想配列のkeyも指定するものとして、値はvalueとします。

// value -> array
const array = [value];

// key, value -> dictionary
const dict = {[key]: value};

連想配列のkeyは[ ]で囲わないと文字列として処理されてしまうので注意が必要です。

要素の追加

// array
data: state.data.concat(action.payload.array)

// dictionary
data: Object.assign(state.data, action.payload.dict);

個人的にObject.assignは連想配列のkeyが重複しても、第2引数で指定した値で上書きしてくれるので使い勝手が良いと感じています。

要素の削除・配列の切り抜き

// array
data: state.data.filter(v => v !== value);

//dictionary
data: state.data.filter(v => v.key !== value);

要素の条件で指定するならfilter()、配列のインデックスに関連して指定するならslice()を用いればいいと思います。

また、単一要素であればfind()とかも使えます。

配列の並び替え

// array
data: state.data.slice().sort()

sort()は破壊的メソッドなのでslice()で一旦コピーしてからソートしています。

また、連想配列に関しては現段階で並び替えるメリットを感じなかったので割愛します。気になったら追記する予定です。

もっと複雑な処理がしたいんだけど…

以前読んだReactの入門本では、switch-case以下の処理を以下のように関数でまとめて処理していましたが、やはりデータの加工はActionCreatorで行いましょう。(前述)

function reducer(state=initialState, action) {
  switch(action.type) {
    case "GET_DATA":
      return getDataReduce(state, action);
    default:
      return state;
  }
}

const getDataReduce(state, action) {
  let newState = {};
  //なんらかの複雑な処理
  return newState;
}

 備考

本記事は、筆者がReactのチュートリアルを受けて詰まった部分、全体で統一しておきたいと思った部分を備忘録として残すことを主の目的としています。

思想に反していたり間違った記述等がありましたら、ガンガン変更していく所存なのでお手柔らかに。

コメント

タイトルとURLをコピーしました