オブジェクトを簡単にイミュータブルに扱えるライブラリ「Immer」をSWRと共に使ってみた

2022.07.08

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

最近はNext.jsを利用したアプリを開発しているのですが、下記のエントリを参考にクライアントアプリ中での状態管理にもSWRを利用しています。

この状態管理をするオブジェクトが特定のオブジェクトの時にうまく動かないことがあり、その解決方法として「Immer」というライブラリが便利だったのでエントリとして書き残しておきたいと思います。

前提条件

アプリはNext.jsを利用したアプリで、SWRを利用しています。 また、今回「Immer」を利用するので以下のようにライブラリを導入した状態を前提としています。

$ yarn add swr immer

困っていたこと

画面上でボタンをクリックすることで画面表示に変更を反映したい、つまり、Stateの変更を行い画面更新をしたい、という状況だったのですが、反映されずに困っていました。

例として、まず以下のようなStateを管理するためのコード用意しておきます。

libs/state-manage.ts

import useSWR from "swr";

export type Trial = {
  patch: number;
  title: string;
  difficulty: string;
  enemy: {
    name: string;
    aggression: string;
  };
};

export const useStaticState = (
  key: string
): [Trial, (trial: Trial) => void] => {
  const defaultTrial: Trial = {
    patch: 2.4,
    difficulty: "Normal",
    title: "Akh Afah Amphitheatre",
    enemy: {
      name: "Shiva, Lady of Forest",
      aggression: "Aggressive",
    },
  };

  const { data: trial, mutate: setTrial } = useSWR(key, null, {
    fallbackData: defaultTrial,
  });

  return [trial as Trial, setTrial];
};

そして、これを画面上でStateとして利用し、ボタンをクリックしたらStateの内容を変更・反映するようなコードとします。

pages/trial-info.tsx

import type { NextPage } from "next";
import { useStaticState } from "../libs/state-manage";

const TrialInfo: NextPage = () => {
  const [trial, setTrial] = useStaticState("trial");

  const applyHotFix = () => {
    const newTrial = { ...trial };
    newTrial.enemy.name = "Shiva, Lady of Frost";

    setTrial(newTrial);
  };

  return (
    <>
      <div style={{ margin: "25px" }}>
        <h1>{trial.title}</h1>
        <div>
          <ul>
            <li>パッチ</li>
            <ul>
              <li>{trial.patch}</li>
            </ul>
            <li>難易度</li>
            <ul>
              <li>{trial.difficulty}</li>
            </ul>
            <li>エネミー</li>
            <ul>
              <li>名前</li>
              <ul>
                <li>{trial.enemy.name}</li>
              </ul>
              <li>攻勢</li>
              <ul>
                <li>{trial.enemy.aggression}</li>
              </ul>
            </ul>
          </ul>
        </div>
        <div>
          <input type="button" onClick={applyHotFix} value="HotFix" />
        </div>
      </div>
    </>
  );
};

export default TrialInfo;

この状態で起動すると、以下のように初期Stateの状態で画面が表示されます。

このとき「名前」はShiva, Lady of Forestですが、実は本当はShiva, Lady of Frostなので「HotFix」ボタンを押して修正しようとします。

が、このままだと何も起きずにLady of Forest(森の女王)のままです…!

何が間違っていたのか?

ここで問題だったのは、以下のコードです。

const newTrial = { ...trial };
newTrial.enemy.name = "Shiva, Lady of Frost";

一見するとスプレッド構文(...)でオブジェクトをコピーしているので良さそうに見えますが、ネストしたオブジェクトなので第1階層のプロパティしか変更検知がされません

救世主がいる

そこでImmerの出番です。

以下のReactのドキュメントでも紹介されていますが、ネストされたオブジェクトのDeep Copyを行いたい時にとても便利なライブラリです。

このImmerを利用してコードを修正してみます。

コードを修正する

以下のようにImmerのproduceを利用してコードを修正してみます。

pages/trial-info.tsx

import type { NextPage } from "next";
import { useStaticState } from "../libs/state-manage";
import produce from "immer";

const TrialInfo: NextPage = () => {
  const [trial, setTrial] = useStaticState("trial");

  const applyHotFix = () => {
    const newTrial = produce(trial, (draft) => {
      draft.enemy.name = "Shiva, Lady of Frost";
    });

    setTrial(newTrial);
  };

  return (
    <>
      <div style={{ margin: "25px" }}>
        <h1>{trial.title}</h1>
        <div>
          <ul>
            <li>パッチ</li>
            <ul>
              <li>{trial.patch}</li>
            </ul>
            <li>難易度</li>
            <ul>
              <li>{trial.difficulty}</li>
            </ul>
            <li>エネミー</li>
            <ul>
              <li>名前</li>
              <ul>
                <li>{trial.enemy.name}</li>
              </ul>
              <li>攻勢</li>
              <ul>
                <li>{trial.enemy.aggression}</li>
              </ul>
            </ul>
          </ul>
        </div>
        <div>
          <input type="button" onClick={applyHotFix} value="HotFix" />
        </div>
      </div>
    </>
  );
};

export default TrialInfo;

では、再度起動して「HotFix」ボタンをクリックしてみましょう。

無事に変更が検知されて、Lady of Frost(氷の女王)になってくれました!

まとめ

以上、オブジェクトを簡単にイミュータブルに扱えるライブラリ「Immer」をSWRと共に使ってみました。

場合によっては、パフォーマンスについて考慮しながらの利用検討になるかもしれませんが、Immerを利用することでオブジェクトを簡単にイミュータブルに扱うことができました。

今後は同様のミスをしないようにImmerを活用していければと思います。

どなたかのお役に立てば幸いです。それでは!

参考