Litの「change-in-update」警告を複数コンポーネントで抑制・解消
Web Awesomeの複数コンポーネントで、Litの開発モード実行時に発生していた「scheduled an update after an update completed」警告を、根本原因の分析にもとづき適切な手段で解消した。
背景
Issue #1269 として報告されていたこの問題では、wa-button や wa-checkbox など多くのコンポーネントが、開発モードのLitランタイムから change-in-update 警告を発していた。同等の機能を持つShoelaceコンポーネントでは発生しないにもかかわらず、Web Awesomeの実装では初期レンダリング時に必ず警告が出る状態だった。
警告の本質は「ある更新サイクルが完了した後に、別の更新サイクルがスケジュールされている」という非効率な動作の検出にある。Web Awesomeでは主に2つのパターンがこの問題を引き起こしていた。1つ目は、WebAwesomeFormAssociatedElement 基底クラスの firstUpdated() 内で呼ばれる updateValidity() が requestUpdate('validity') をトリガーするパターン。2つ目は、HasSlotController がスロットの変化(slotchangeイベント)を検知して requestUpdate() を呼び出すパターンだ。これらはいずれも「DOMが利用可能になって初めて状態を確定できる」という本質的な制約から生じている。
これらの更新は機能的には必要なものであり、警告の原因を取り除くことは設計上困難であることから、今回の修正では問題の種類に応じて異なる対処方針が採られた。
技術的な変更
今回の修正は大きく3つのアプローチに分類できる。
① disableWarning('change-in-update') の追加(多数のコンポーネント)
最も多く採用されたアプローチは、Litが提供する disableWarning 静的メソッドを使って警告を明示的に抑制するものだ。対象コンポーネントは以下のとおり。
wa-buttonwa-checkboxwa-color-pickerwa-inputwa-number-inputwa-progress-ringwa-radiowa-radio-groupwa-selectwa-switchwa-textareawa-tree-item
各ファイルのクラス定義直後に以下のような1行が追加されている。
WaButton.disableWarning?.('change-in-update');
オプショナルチェーン(?.)を使用しているのは、disableWarning が開発ビルドのみで利用可能なAPIであるためだ。本番ビルドではこのメソッドが存在しないため、?. により安全に無視される。各コンポーネントには詳細なコメントが付記されており、なぜその警告が不可避なのかが説明されている。
② updated() から willUpdate() へのリファクタリング(wa-card、wa-slider)
一部のコンポーネントでは、updated() 内で状態プロパティを変更していたことが警告の原因だったため、より適切なライフサイクルメソッドへ移行した。
wa-card では、スロットの存在チェックにもとづいて withHeader や withMedia プロパティを更新するロジックが updated() から willUpdate() に移動された。
// 変更前
updated() {
if (!this.withHeader && this.hasSlotController.test('header')) this.withHeader = true;
if (!this.withMedia && this.hasSlotController.test('media')) this.withMedia = true;
// ...
}
// 変更後
willUpdate() {
if (!this.withHeader && this.hasSlotController.test('header')) this.withHeader = true;
if (!this.withMedia && this.hasSlotController.test('media')) this.withMedia = true;
// ...
}
wa-slider はより大規模なリファクタリングが行われた。updated() に散在していた range 変更時の requestUpdate() 呼び出し、min/max 変更時のクランプ処理などが willUpdate() に集約された。また、min/max の変更に対してもクランプ処理が適用されるよう、changedProperties.has('min') と changedProperties.has('max') の条件チェックが追加されている。
③ ビルド設定への conditions 追加(build.js)
disableWarning が機能するためには、Litの開発ビルドが読み込まれている必要がある。packages/webawesome/scripts/build.js に以下の変更が加えられ、開発時には development エクスポート条件が有効化されるようになった。
const config = {
conditions: isDeveloping ? ['development'] : [],
format: 'esm',
// ...
};
これにより、isDeveloping フラグが true の場合に限りLitの開発ビルドが選択され、disableWarning APIを含む開発用の機能が利用可能になる。
設計判断
警告の抑制と根本的な解消を使い分けた点がこの修正の核心にある。
willUpdate() への移行は、更新サイクル内で状態変更を完結させる「正しい修正」だ。しかし wa-progress-ring のように、updated() でしか実行できない処理(DOMが描画された後でなければ getComputedStyle() が取得できないSafariのワークアラウンド)や、フォームバリデーションのように「レンダリング後に検証ターゲットを参照する必要がある」処理では、この手法は使えない。そうしたケースに対しては disableWarning を選択している。
重要なのは、disableWarning の使用が「警告を黙らせる」だけでなく、詳細なコードコメントを通じて「なぜこの警告が不可避なのか」を明示的にドキュメント化している点だ。将来のメンテナが警告の意図を誤解して削除したり、逆に不必要な最適化を試みるリスクを低減する設計になっている。
disableWarning?.() のオプショナルチェーンは、同一のソースコードが開発・本番両ビルドで動作するための必須の配慮であり、ビルドフラグによる条件分岐なしに両環境を透過的にサポートする。
まとめ
本PRは、Litの更新ライフサイクルに関する深い理解をもとに、「修正可能な箇所は willUpdate() で修正し、構造的に不可避な箇所は根拠を明示した上で disableWarning で抑制する」という一貫した方針を採った。開発者体験のノイズを除去しつつ、設計の意図をコードベースに記録した実用的な修正といえる。