かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( MobX を使用してみる2 )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( MobX を使用してみる ) の続きです。

参照したサイト・書籍

  1. danielearwicker / computed-async-mobx
    https://github.com/danielearwicker/computed-async-mobx

目次

  1. autorun を複数定義するとどのように動作するのか?
  2. computed-async-mobx を使用して API を呼び出す非同期処理をクラス内に記述する
  3. action で API を呼び出す非同期処理をクラス内に記述する
  4. reaction で API を呼び出す非同期処理をクラス内に記述する
  5. reaction で複数のプロパティを監視するには [] で囲む

手順

autorun を複数定義するとどのように動作するのか?

mobx.autorun(...) を複数定義すると最後に定義されたものが最初に実行されるようです。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("autorun を複数定義すると最後に定義したもの→最初に定義したものの順に実行される", () => {
        document.body.innerHTML = `
            <div id="value"></div>
            <div id="data"></div>
            <div id="all"></div>
        `;

        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: "",
                    data: "",
                    all: mobx.computed(() => `${this.value}, ${this.data}`)
                });
            }
        }

        const sample = new Sample();
        mobx.autorun(() => {
            console.log("PASS1");
            $("#value").text(sample.value);
        });
        mobx.autorun(() => {
            console.log("PASS2");
            $("#data").text(sample.data);
        });
        mobx.autorun(() => {
            console.log("PASS3");
            $("#all").text(sample.all);
        });

        expect($("#value").text()).toBe("");
        expect($("#data").text()).toBe("");
        expect($("#all").text()).toBe(", ");

        console.log("★★★");
        sample.value = "1";
        expect($("#value").text()).toBe("1");
        expect($("#data").text()).toBe("");
        expect($("#all").text()).toBe("1, ");

        console.log("●●●");
        sample.data = "a";
        expect($("#value").text()).toBe("1");
        expect($("#data").text()).toBe("a");
        expect($("#all").text()).toBe("1, a");
    });

テストを実行すると PASS3 → PASS1、PASS3 → PASS2 と最後に定義されたものから順に実行されることが確認できます。

f:id:ksby:20180106204147p:plain

computed-async-mobx を使用して API を呼び出す非同期処理をクラス内に記述する

プロパティの値が更新されたら外部の API を呼び出して別のプロパティの値を更新する処理を mobx.autorunAsync(...) で定義してもいいのですが、mobx.computed(...) のようにクラス内で定義できると便利かなと思って調べたところ、danielearwicker/computed-async-mobx というモジュールを見つけましたので試してみます。

npm install --save computed-async-mobx コマンドを実行してインストールします。

f:id:ksby:20180106205528p:plain

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加します。

    test("computed-async-mobx を使用するとクラス内に API 呼び出しの非同期処理を定義できる", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        // API を呼び出す処理を mobx.autorunAsync(...) ではなく、
        // computed-async-mobx.asyncComputed(...) を使用してクラス内に定義する
        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: computedAsyncMobx.asyncComputed("", 0, async () => {
                        console.log("PASS1-1");
                        if (this.name === "") {
                            return "";
                        }
                        console.log("PASS1-2");
                        console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
                        const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(this.name);
                        const json = res.data;
                        return json.weather[0].main;
                    })
                });
            }
        }

        const area = new Area();
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            // area.weather ではなく area.weather.get() で値を取得する点に注意する
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather.get());
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 1100);
    });

テストを実行すると、以下のことが分かります。

  • computedAsyncMobx.asyncComputed(...) は初回定義時に1度実行される(console.log("(1) ..."); の前に "PASS1-1" がログに出力されている)。
  • $("#name").val() を更新すると (1)~(5) の順で実行される。

f:id:ksby:20180106223021p:plain

action で API を呼び出す非同期処理をクラス内に記述する

MobX のマニュアルを読んでいたら Writing asynchronous actions のページを見つけました。mobx.action(...) を使用すれば API を呼び出す非同期処理をクラスのメソッドとして実装できます。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("mobx.action を使用して API 呼び出しの非同期処理を記述する", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        // API を呼び出す処理を mobx.autorunAsync(...) ではなく、
        // mobx.action(...) を使用してクラス内に定義する
        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: "",
                    getWeather: mobx.action(async () => {
                        console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
                        const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(this.name);
                        const json = res.data;
                        this.weather = json.weather[0].main;
                    })
                });
            }
        }

        const area = new Area();
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather);
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
            // action は自動実行はされないので、ここで area.getWeather(); を呼び出す
            area.getWeather();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 1100);
    });

テストを実行すると、(1)~(5) の順で処理が進むことが確認できます。

f:id:ksby:20180107070022p:plain

reaction で API を呼び出す非同期処理をクラス内に記述する

mobx.action(...) でもいいのですが、MobX の機能だけでプロパティを更新したら自動的に API を呼び出して天気を取得するように出来ないかマニュアルを見ていたところ、Reaction というページを見つけました。mobx.reaction(...) で監視対象のプロパティと実行する処理を記述すればよいようです。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("mobx.reaction を使用すれば、プロパティ更新時に自動で非同期処理を実行できる", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        // API を呼び出す処理を mobx.autorunAsync(...) ではなく、
        // mobx.reaction(...) を使用してクラス内に定義する
        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: ""
                });
                mobx.reaction(
                    () => this.name,
                    async () => {
                        console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
                        const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(this.name);
                        const json = res.data;
                        this.weather = json.weather[0].main;
                    }
                );
            }
        }

        const area = new Area();
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather);
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 1100);
    });

テストを実行すると、(1)~(5) の順で処理が進むことが確認できます。computedAsyncMobx.asyncComputed(...)` を使わなくてもこれで良さそうです。

f:id:ksby:20180107071154p:plain

またプロパティを再度更新したら API を呼び出す処理が実行されるのか、テストを以下のように修正してcomputedAsyncMobx.asyncComputed(...)mobx.action(...)mobx.reaction(...) の3パターンで試してみましたが、これは全て API を呼び出す処理を実行しました。mobx.reaction(...)mobx.when(...) と同じで1度しか実行されないのでは?と思いましたが、そうではないようです。

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
            $("#name").val("Oosaka").blur();
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 2000);
  • doneFlg = true; の後に $("#name").val("Oosaka").blur(); を追加します。
  • done(); を呼び出す時の遅延時間を 1100 → 2000 へ変更します(1100 のままだと動作しない場合があったため)。

reaction で複数のプロパティを監視するには [] で囲む

mobx.reaction(...) で複数プロパティを監視したい場合には配列で指定するとできるようです。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("mobx.reaction で複数プロパティを監視するには [] で囲んで指定する", () => {
        document.body.innerHTML = `
            <div id="sample"></div>
        `;

        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: "",
                    data: "",
                    pass: ""
                });
                mobx.reaction(
                    () => [this.value, this.data, this.pass],
                    ([value, data, pass]) => {
                        $("#sample").text(`value = ${value}, data = ${data}, pass = ${pass}`);
                    }
                );
            }
        }

        const sample = new Sample();
        console.log("★★★ " + $("#sample").text());
        sample.value = "a";
        console.log("★★★ " + $("#sample").text());
        sample.data = "1";
        console.log("★★★ " + $("#sample").text());
        sample.pass = "PASS1";
        console.log("★★★ " + $("#sample").text());
        sample.value = "b";
        console.log("★★★ " + $("#sample").text());
        sample.data = "2";
        console.log("★★★ " + $("#sample").text());
        sample.pass = "PASS2";
        console.log("★★★ " + $("#sample").text());
    });

実行すると、mobx.reaction(...) が実行されて反映されることが確認できます。

f:id:ksby:20180109004444p:plain

履歴

2018/01/09
初版発行。