2020.03.03

LWCのリアクティブプロパティと再レンダリングの関係は複雑だった(第1回)

はじめに

みなさん、Lightning Web Component で開発してますか?
開発している方、リアクティブプロパティを使いこなしてますか?

リアクティブプロパティについて、ガイドを参照すると、だいたい以下のようなことが記載されています。
@track や @api といったデコレータが付与されているプロパティのことで、
リアクティブプロパティの値が変更されると、コンポーネントが再レンダリングされます。
コンポーネントが再レンダリングされると、テンプレートで使用されるすべての式が再評価されます。
一見、デコレータを記載するだけで、自動的に値の変更を追跡して再レンダリングして画面に反映してくれるという便利な機能のように思われます。
しかし、実際にLWCの開発をしてみると、リアクティブプロパティと再レンダリングの関係の理解不足から思わぬ動作に悩まされることがありました。
その中から2つの事例をもとに、調査した結果についてテラスカイ亀山田伏が全3回にわたり紹介したいと思います。
第1回:事例その1、どうしたら値は変更されたとみなされるのか?
第2回:事例その2、値が変更されても再レンダリングされないこともある?
第3回:複雑な関係をまとめてみた、これで使いこなせるのか?

今回は、その第1回目となります。

本題に入る前に

今回、実際にLWCの開発の中で直面した事例を紹介するにあたり、説明の都合上、事象を再現できる簡単な事例に置き換えました。
そのため、実際に開発している方には共感できないような事例になっているかもしれませんが、ご容赦ください。
今回ご紹介しているコードについては、Lightning Web Components Playgroundで動作させることが可能なものとなっていますので、実際の動作を確認しながら読み進めていただくとイメージがしやすいと思います。

参考記事

もし、LWCについての前提知識に不安がある方は、基本的なことを記載した過去の記事もありますので、参考にしてください。

 ~事例その1、どうしたら値は変更されたとみなされるのか?~

名前を入力するコンポーネントに対して、入力した内容をクリアするボタンを追加するという事例を元に、調査結果を紹介していきます。

現状を確認してみましょう

<template>
    <div class="app slds-p-around_x-large">
        <lightning-layout vertical-align="end">
            <lightning-layout-item>
                <lightning-input label="名前" value={name}></lightning-input>
            </lightning-layout-item>
        </lightning-layout>
    </div>
</template>
app.html
import { LightningElement, track } from 'lwc';

export default class App extends LightningElement {
    @track
    name;
}
app.js
「name」プロパティをリアクティブプロパティとし、名前入力欄にバインドしている状態です。

クリアボタンを追加してみましょう

<template>
    <div class="app slds-p-around_x-large">
        <lightning-layout vertical-align="end">
            <lightning-layout-item>
                <lightning-input label="名前" value={name}></lightning-input>
            </lightning-layout-item>
            <!-- 追加 始-->
            <lightning-layout-item>
                <lightning-button-icon icon-name="utility:close" onclick={handleClick}></lightning-button-icon>
            </lightning-layout-item>
            <!-- 追加 終 -->
        </lightning-layout>
    </div>
</template>
app.html
import { LightningElement, track } from 'lwc';

export default class App extends LightningElement {
    @track
    name;

    /** 追加 始 */
    handleClick() {
        this.name = '';
    }
    /** 追加 終 */
}
app.js
HTMLに対してはボタンを追加し、JSにはボタン押下時のイベントハンドラを追加しています。
イベントハンドラには、名前入力欄にバインドされている「name」プロパティに空文字を設定する処理を実装しました。
「name」プロパティは、リアクティブプロパティとなっていますので、再レンダリングされて値がクリアされるはずです。

実際に入力した値がクリアできるか試してみましょう

「AAA」 と入力して、ボタンを押下すると、値が無事クリアされました。
では次に「BBB」と入力して、ボタンを押下してみると、何故でしょうか、値がクリアされません。。。

何故なのでしょうか?

結論としては、以下の2つの仕様が事象の原因でした。

1. 入力欄の値(のvalue値)を変えても、バインドされているプロパティの値は変わらない (一方向バインド)

2. リアクティブプロパティに前回の設定値と異なる値を設定した場合にのみ、再レンダリングされる
クリアできなかったところに上記の仕様を当てはめてみましょう。
入力欄に「BBB」と入力しましたが、 バインドされているプロパティの値は、一度クリアした後の「''」のまま値は変わりません。(1.の仕様)
クリアボタンで空文字を設定しても、前回の設定値と同じ「''」のため、再レンダリングされなかったということです。(2.の仕様

※1回目はクリアできて、2回目がクリアできないのは、JSの初期値とクリア値の違いによるものです。上記動作確認時のnameプロパティの初期値はundefinedですが、理解のしやすさを考慮し、以降は初期値を「AAA」として説明します

※画面上で「AAA」→「BBB」と値が変更されても、 <lightning-input>コンポーネントにバインドされたプロパティの値は、「AAA」という認識

もう少し詳しく見ていきましょう

lightning-inputもコンポーネントなので、通常の親子関係に当てはめ、value属性がパブリックリアクティブプロパティであるとイメージして考えてみます。

1回目のクリアではapp.jsの「name」プロパティが「'AAA'」→「''」へと値が変化したため、appコンポーネントが再レンダリングされますが、
2回目のクリアではapp.jsの「name」プロパティは「''」→「''」へと、値の変化がないため、appコンポーネントは再レンダリングされず、lightning-inputコンポーネントの値はクリアされません。

appコンポーネントは再レンダリングされているの?

appコンポーネントが再レンダリングされたかを確認するために、「name」プロパティを直接バインドした要素を追加します。(lightning-inputのように子コンポーネントにならないもの)
初期値は、「undefined」と値がそのまま表示されます。

<template>
    <div class="app slds-p-around_x-large">
        <lightning-layout vertical-align="end">
            <lightning-layout-item>
                <lightning-input label="名前" value={name}></lightning-input>
            </lightning-layout-item>
            <lightning-layout-item>
                <lightning-button-icon icon-name="utility:close" onclick={handleClick}></lightning-button-icon>
            </lightning-layout-item>
        </lightning-layout>
        <!-- 追加 始-->
        <input value={name}>
        <!-- 追加 終 -->
    </div>
</template>
app.html
この入力欄についても2回目以降は同様にクリアはされません。

そのため、appコンポーネントの再レンダリングが行われていないことが分かります。

値を明示的に変えてみましょう

クリア押下時に「name」プロパティが常に変化するよう、「'1'」 を設定後に、「''」 を設定したらどうなるのでしょうか?
import { LightningElement, track } from 'lwc';

export default class App extends LightningElement {
    @track
    name;

    handleClick() {
        /** 追加 始 */
        this.name = '1';
        /** 追加 終 */
        this.name = '';
    }
}
app.js
この変更により、クリア時にapp.jsの「name」プロパティに「'1'」を設定したタイミングで、値が変化したと判定され、appコンポーネント自身は再レンダリングされるようになりました。
(追加した入力欄の値は、2回目以降もクリアされるようになります。)

しかし、lightning-inputの入力値はまだクリアされていません。
appコンポーネントが再レンダリングされる際のapp.jsの「name」プロパティの値が 「''」 であり、前回バインドした「''」と変更がないと判定されてlightning-inputへは値が設定されなかったことが原因です。

どのようにしたらクリアできるのでしょうか?

ここまでの調査結果により、クリア処理の動作には、
1.appコンポーネントの再レンダリング
2.lightning-inputの再レンダリング
が必要であることがわかりました。

それぞれ再レンダリングされる条件は
1.クリア処理内で、appコンポーネントの「name」プロパティに「''」を設定する前に、「''」と異なる値が設定されていること
2.コンポーネントのvalue値に対して、前回バインドされた値が、「''」以外の値であること
です。

そこで2つの条件を満たすような対応方法を1つご紹介します!

対応方法とは?

<lightning-input>にchangeイベントのハンドラを追加して、入力された値が常に「name」プロパティに設定されるようにします。
※さきほど確認のために追加した、 <input>は削除してください。
<template>
    <div class="app slds-p-around_x-large">
        <lightning-layout vertical-align="end">
            <lightning-layout-item>
                <!-- onChange 追加-->
                <lightning-input label="名前" value={name} onchange={handleChange}></lightning-input>
            </lightning-layout-item>
            <lightning-layout-item>
                <lightning-button-icon icon-name="utility:close" onclick={handleClick}></lightning-button-icon>
            </lightning-layout-item>
        </lightning-layout>
    </div>
</template>
app.html
import { LightningElement, track } from 'lwc';

export default class App extends LightningElement {
    @track
    name;

    handleClick() {
        this.name = '';
    }

    /** 追加 始 */
    handleChange(event) {
        this.name = event.target.value;
    }
    /** 追加 終 */
}
app.js

クリアできましたか?

おそらく想定通りの動きになったと思います。

無事にクリアするボタンを追加することができました。
お疲れさまでした!

おわりに

最後までお読み頂き、ありがとうございました。

次回は、
第2回:事例その2、値が変更されても再レンダリングされないこともある?
を予定しています!
57 件
     
  • banner
  • banner

関連する記事