SPIDERPLUS Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

Reactコンポーネント以外のロジックコードのHMR化

はじめに

プラットフォーム開発部のわにわに🐊です。

今回は、前回の記事の続きで、Reactコンポーネント以外のロジックのjsをHMR対応した時のことを書きます。

techblog.spiderplus.co.jp

 

ReactコンポーネントはViteのプラグイン(@vitejs/plugin-react)を使うことでHMR対応できますが、そのままではその他のjsファイルはHMR対応とならず、コードを変更したときにページ全体リロードが発生してしまいます。

なので、Reactコンポーネント以外のロジックコードをHMR対応できると便利です。

 

対応方針

ここで2つの選択肢を考えました。

  • 自前モジュールをHMRの正規の方法でHMR対応する
  • ロジックだけを格納するReactコンポーネントを作成してしまう

もちろん王道は前者なのですが、HMR APIをそれなりに理解をしてないとできない

- 公式ドキュメント
- 参考ページ: Hot Module Replacement is Easy ← 全然easyじゃない

ので、後者を採用し、既にできている仕組みに手っ取り早く乗りたいと思います。

もし、今後HMR APIによるコードのHMR化を紹介できる機会があれば書いてみたいと思います。

ですので以下は、ロジックだけを格納するReactコンポーネントを作成する方法について書いていきます。

前提(避けなければいけない障害物)

  1. Reactのコンポーネントを定義しているファイルでコンポーネント関数以外のメンバをexportすると、そのファイルのHMRが効かなくなる。
        - コンポーネントの中しか変更しない場合でも、全体をリロードしてしまう。
        - 追加したexportが、他からimportされていなくてもHMRが無効になる。

  2. 変更前のコードの関数を直接参照していると、ホットリロードが動作した後も変更前の関数が呼ばれてしまう。

これらを知らないと結構ハマります。
回避に少し工夫が必要ですので解説していきます。

ロジックだけを格納するReactコンポーネントを作成する(純関数のみの場合)

Reactコンポーネントにロジックを以下のように同居させます。
前提1の問題があり、ロジック関数を直接exportするとHMRが解除されてしまうため、コンポーネントのメンバとしてロジック関数を定義します。

// JikkenLogic.jsx
import React from 'react';
import Logic from './Logic';

export default function JikkenLogic() {
    Logic.Jikken = JikkenLogic;
    return <></>;
}

JikkenLogic.hello = () => {
    return 'hello';
};

JikkenLogic.add = (a, b) => {
    return a + b;
};

ロジックを利用したいコードの方で

JikkenLogic.hello();  // NGの書き方

などとしてしまうと、前提2の問題が発生しコードを変更してホットリロードが発生しても、変更前の関数が呼ばれてしまいます。

それを回避するために書いているのがコンポーネント関数内の

Logic.Jikken = JikkenLogic;  // 新しいモジュールに差し替えている

の部分です。

Logicは単にコンポーネント関数を格納するだけのオブジェクトですが、ホットリロード発生時にコンポーネント関数がReactにより再実行されるため、その中で自分を格納することで、前提2の問題を回避できます。

これは、ロジックを利用したいコードの方で

JikkenLogic.hello();  // NGの書き方

と直接呼び出してしまうと、このコードの部分のJikkenLogicは、HMRによる入れ替え前のロジックに固定されてしまうので

Logic.Jikken.hello();  // OKの書き方

とできるように`Logic.Jikken`のモジュールを差し替えているのです。 それで、コード変更後のホットリロード時にちゃんと変更後の関数が呼ばれるようになります。

メソッドを増やしたり減らしたりしてもHMRが効いてリロードせずにロジック改変できるので、かなり便利です。

ロジックだけを格納するReactコンポーネントを作成する(クラスが欲しい場合)

純関数のセットだけでなく、クラスをHMR化したインスタンスも欲しい場合は以下のようにします。 これもexportしているのはコンポーネント関数のみです。(前提1の問題を回避するため)

import React from 'react';
import Logic from './Logic';

export default function JikkenClass() {
    if (this?.constructor === JikkenClass) {
        return new Jikken();
    }
    Logic.Jikken = JikkenClass;
    return <></>;
}

class Jikken {
    value = 0;

    inc(i) {
        this.value += i;
    }

    getValue() {
        return this.value;
    }
}

この場合、ロジックを利用したいコードの方で

const jikken1 = new Logic.Jikken();
const jikken2 = new Logic.Jikken();
jikken1.inc(1);
jikken1.inc(3);
jitken2.inc(2);
const result1 = jikken1.getValue(); // 4
const result2 = jikken2.getValue(); // 2

といい感じに使えて、しっかりHMR対応してます。

注意:一度インスタンス化してしまったオブジェクトは、その後のHMRで再生成されることはないのでコード変更前のコードが動作します。これを回避するためにデータとロジックを分離する作戦もあります。(ユニットテストもしやすいので私は好きです。)

実装を見るとコンポーネント関数内の

if (this?.constructor === JikkenClass) {

がポイントです。

jsでは、functionがnew付きで呼ばれた場合、this.constructorが関数自身になるので、インスタンス化させたい意思があるかを判定できます。

なので、ホットリロード時にReactによりコンポーネント関数が再実行されるときは、`new`なしで呼ばれるので、ifに入らずにLogicに自分を格納し、`<></>`をReactに返します。
`new`付きで呼ばれるときは、欲しいクラスのインスタンスを返します。

ロジック用コンポーネントのReactへの登録

上記のようにコンポーネント関数を作っても、Reactに登録されていないと実行されないので当然HMRも効きません。
なので、どこでもいいのでReact管理下にタグ

<JikkenLogic />

のようにロジックを書き込んだコンポーネントを追加しておいてください。

終わりに

これで、様々なロジックをHMR対応して、ページリロードせずに変更を確認できるので、効率的に開発できるようになりました。

UIでいろんな操作をしてやっと到達できる機能の開発がいままで面倒だったのですが、これでかなり楽になりました。

スパイダープラスでは仲間を募集中です。 日々の工夫で一緒に開発を楽しくしていきましょう! ぜひ、お気軽にご連絡ください。