あぶすとらくと
Redux を使っていると、①ネストの深いところにあるオブジェクトについて、②(元のオブジェクトは変更せずに)複数のプロパティを変更したオブジェクトを作成したい時ってありますよね。
Redux Toolkit で使われている Immer というライブラリのおかげで、①再帰的に長くなるネストを書かなくても良くなりますが、②アップデートされる部分でスプレッド構文の場合よりも繰り返しが少し多くなってしまいます。
そこで、最近影の薄いObject.assign()メソッドを使って、アップデート部分の記述を DRY (Don't Repeat Yourself) にすることを試みました。
環境
- Redux Toolkit 1.5.1
- React Redux 7.2.3
- Typescript 4.0
さらばスプレッド地獄
Redux の State はイミュータブルでなければならないので、状態を変化させるためにスプレッド構文を使うことになるのですが、Redux Toolkit は Immer というライブラリを使用していて、これが私たちをスプレッド地獄から解き放ってくれます。
  return { 
    ...state,
    textFields: {
      ...state.textFields,
      [fieldName]: {
        ...state.textFields[fieldName],
        value,
        isUntouched: false,
      }
    }
  }
Immer が無ければ、このように、再帰的にチェーンが長くなっていく地獄みたいな return 文を書かなければいけませんでしたが、
const {fieldName, value} = action.payload;
state.textFields[fieldName].value = value;
state.textFields[fieldName].isUntouched = false;
Immer のおかげでこのようにチェーンの重複した記述を取り除く事ができます。
見た目からは、オブジェクトのプロパティーを手続き的に変更しているように見えますが、 Immer が魔法の力で新しいオブジェクトをイミュータブルに複製してくれています。ありがたい。
コード全体
interface TextField {
  value: string;
  isUntouched: boolean;
  isDisabled: boolean;
}
interface State {
  textFields: {
    name: TextField;
  }
}
type TextFieldName = keyof State["textFields"];
const initTextField = (
  value: string = "", 
  isUntouched: boolean = true, 
  options?: {isDisabled?: boolean}
): TextField => ({
  value, isUntouched, isDisabled: options?.isDisabled ?? false
});
const initialState: State = { textFields: {name: initTextField() }};
const slice = createSlice({
  name: "name-form",
  initialState,
  reducers: {
    // 入力した時にこのアクションが呼ばれる
    inputString(state, action: PayloadAction<{ fieldName: TextFieldName, value: string }>) {
      const {fieldName, value} = action.payload;
      state.textFields[fieldName].value = value;
      state.textFields[fieldName].isUntouched = false;
    },
  },
};
それでもまだ、DRYじゃない
変更するべきプロパティが複数あるので、state.textFields[fieldName]を何度も書かないといけないのが面倒ですよね。
存在しない娘の「パパの書いたReducer、ぜんっぜんDRYじゃないね!」という声が聞こえてきそうです。
そんな時は、下のコードのように、その部分を一度変数に入れてしまいましょう。
そうしても、 Immer はきちんと新しいオブジェクトを作ってくれます。かしこいですね。
inputString(state, action: PayloadAction<{ fieldName: TextFieldName, value: string }>) {
  const {fieldName, value} = action.payload;
  const field = state.textFields[fieldName];
  field.value = value;
  field.isUntouched = false;
}
それでも僕は、欲張りなので
Immer によって、チェーンが消えて、アップデートする部分のみに着目するだけでよくなり、記述が楽になったのは良いですが、スプレッド構文の持ち味である、Shorthand property が使えなくなってしまいました。
state.textFields[fieldName] = { ...state.textFields[fieldName], value, isUntouched: false };
僕は欲張りなので両方のいいとこ取りをしたい。
そこで、Object.assign()の登場です。
inputString(state, action: PayloadAction<{ fieldName: TextFieldName, value: string }>) {
  const { fieldName, value } = action.payload
  Object.assign(state.textFields[fieldName], { value, isUntouched: false })
},
Object.assign(target, source1, ..)メソッドは、シャローコピーのみを行うスプレッド構文とは違い、targetオブジェクトをアップデートしてくれる関数で、source1 etc. オブジェクトのプロパティの値をtargetのオブジェクトに代入して上書きしてくれます。 Immerとは相性抜群ですね。
これで、最大限に DRY な Reducer の完成です!
めでたしめでたし。
参照記事
- 
Immer の Github リポジトリにあるテストコード
- テストの中でObject.assignを使った箇所があります。
 
- テストの中で
