新しいオブジェクトを生成するときに{...state, hoge}といちいち書きたくないあなたに、Immerjs

2 min read読了の目安(約2300字

対象となる読者

React Hookで使用するReducer関数のように、既存のオブジェクトをベースにして新規のオブジェクトを生成する際に、スプレッド構文({...state, hoge}という書き方)で書いているけど、コードが冗長になって困っている方。
React HookのReducer関数を使用しながら説明をするので、React HookでReducerの関数を作成したことがある方が対象となります。

https://ja.reactjs.org/docs/hooks-reference.html#usereducer

Reducerでスプレッド構文で新しいStateを返す方法

React HookでReducerの関数を作成する際,下記のようにActionのtypeに応じて、既存のStateとは別に新しいstateを返すような実装をすることが多いかと思います。

const initialState = {
  isLoading: false,
  hoge: "hoge"
}
reducer(state = initialState, action) => {
  switch (action.type) {
    case "UPDATE":
      return {...state, hoge: "new_hoge"};
    default:
      return state;
  }
}

上記のようにステートの構成がシンプルならいいのですが、下記のように階層が深いステートの場合だとどうでしょうか。

const initialState: State = {
  hoge1: {
    fuga1: {
      piyo1: "test",
      piyo2: "test",
    },
    fuga2: "test",
  },
  hoge2: {
    fuga3: "test",
  },
};

const todos = (state = initialState, action) => {
  switch (action.type) {
    case "UPDATE":
      return {
        ...state,
        hoge1: {
          ...state.hoge1,
          fuga1: {
            ...state.hoge1.fuga1,
            piyo1: "new_test",
          },
        },
      };
    default:
      return state;
  }
};

上記のように、本来state.hoge1.fuga1.piyo1の値を変えたいだけなのに、{...state, hoge: "new_hoge"}のようなスプレッド構文をそこら中に書く必要が出てきてしまいます。

Immerjsで新しいStateオブジェクトを生成する

そこで新しいStateオブジェクトをイミュータブルに生成する方法として、下記のImmerjsというライブラリを使用してみます。

https://github.com/immerjs/immer
import produce from "immer";

const initialState: State = {
  hoge1: {
    fuga1: {
      piyo1: "test",
      piyo2: "test",
    },
    fuga2: "test",
  },
  hoge2: {
    fuga3: "test",
  },
};

const todos = (state = initialState, action) => {
  switch (action.type) {
    case "UPDATE":
      return produce(state, (draftState) => {
        draft.hoge1.fuga1.piyo1 = "new_test";
      });
    default:
      return state;
  }
};

produceという関数が肝です。
第一引数で渡したオブジェクトが、第二引数のメソッドにdraftStateとして渡されます。
このメソッドないでプロパティを書き換えると、既存のstateに影響を与えることなく新しいオブジェクトを生成することができるようになります。

const newState = produce(state, (draftState) => {
  draft.hoge1.fuga1.piyo1 = "new_test";
});
console.log(state.hoge1.fuga1.piyo1); // test(値はそのまま)
console.log(newState.hoge1.fuga1.piyo1); // new_test(値が更新されている)

まとめ

Immerjsについて、最初は関数でイミュータブルなオブジェクトを生成することに慣れないかもしれませんが、慣れるとスプレッド構文に比べるとかなりスッキリ書けるので、ぜひ試してみてください。