7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

あぶすとらくと

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 ToolkitImmer というライブラリを使用していて、これが私たちをスプレッド地獄から解き放ってくれます。

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

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

immerのおかげで
const {fieldName, value} = action.payload;
state.textFields[fieldName].value = value;
state.textFields[fieldName].isUntouched = false;

Immer のおかげでこのようにチェーンの重複した記述を取り除く事ができます。
見た目からは、オブジェクトのプロパティーを手続き的に変更しているように見えますが、 Immer が魔法の力で新しいオブジェクトをイミュータブルに複製してくれています。ありがたい。

コード全体
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;
    },
  },
};

それでもまだ、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 が使えなくなってしまいました。

むりにShorthandを使うとこうなる
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 の完成です!
めでたしめでたし。

参照記事

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?