Redux Toolkit (Immer) で、複数のプロパティをまとめて変更する

# あぶすとらくと

Redux を使っていると、①ネストの深いところにあるオブジェクトについて、②（元のオブジェクトは変更せずに）複数のプロパティを変更したオブジェクトを作成したい時ってありますよね。
[Redux Toolkit](https://redux-toolkit.js.org/) で使われている [Immer](https://immerjs.github.io/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](https://redux-toolkit.js.org/) は [Immer](https://immerjs.github.io/immer/) というライブラリを使用していて、これが私たちをスプレッド地獄から解き放ってくれます。

```ts:地獄
  return { 
    ...state,
    textFields: {
      ...state.textFields,
      [fieldName]: {
        ...state.textFields[fieldName],
        value,
        isUntouched: false,
      }
    }
  }
```

Immer が無ければ、このように、再帰的にチェーンが長くなっていく地獄みたいな return 文を書かなければいけませんでしたが、

```ts:immerのおかげで
const {fieldName, value} = action.payload;
state.textFields[fieldName].value = value;
state.textFields[fieldName].isUntouched = false;
```
Immer のおかげでこのようにチェーンの重複した記述を取り除く事ができます。
見た目からは、オブジェクトのプロパティーを手続き的に変更しているように見えますが、 Immer が魔法の力で新しいオブジェクトをイミュータブルに複製してくれています。ありがたい。

<details><summary>コード全体</summary><div>

```ts:store/name-form.ts
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;
    },
  },
};
```
</div></details>


# それでもまだ、DRYじゃない
変更するべきプロパティが複数あるので、`state.textFields[fieldName]`を何度も書かないといけないのが面倒ですよね。
~~存在しない娘の「パパの書いたReducer、ぜんっぜんDRYじゃないね！」という声が聞こえてきそうです。~~
そんな時は、下のコードのように、その部分を一度変数に入れてしまいましょう。
そうしても、 Immer はきちんと新しいオブジェクトを作ってくれます。かしこいですね。

```ts:ちょっと修正
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 が使えなくなってしまいました。

```ts:むりにShorthandを使うとこうなる
state.textFields[fieldName] = { ...state.textFields[fieldName], value, isUntouched: false };
```

僕は欲張りなので両方のいいとこ取りをしたい。
そこで、`Object.assign()`の登場です。

```ts:最終
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 の完成です！
めでたしめでたし。

# 参照記事

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

https://redux-toolkit.js.org/usage/immer-reducers

- [Immer の Github リポジトリにあるテストコード](https://github.com/immerjs/immer/blob/9257084c2b3c04fc9727e4c701d672483a7767b7/__tests__/curry.js)
  - テストの中で`Object.assign`を使った箇所があります。