Salesforce LWCイベント通信:bubblesとcomposedを活用したコンポーネント間連携マスターガイド

執筆者:Salesforce 開発者


背景と応用シナリオ

Salesforceプラットフォーム上で最新のユーザーインターフェースを構築するためのフレームワークであるLightning Web Components (LWC) (ライトニングウェブコンポーネント)は、そのコンポーネントベースのアーキテクチャが最大の特徴です。アプリケーションは、自己完結型で再利用可能な小さなコンポーネントを組み合わせて構築されます。このアプローチは開発効率と保守性を大幅に向上させますが、同時に「コンポーネント間でどのように情報をやり取りするか」という新たな課題を生み出します。

例えば、顧客リストを表示する親コンポーネントがあるとします。リストの各行は、顧客情報を表示し、詳細表示ボタンを持つ子コンポーネントです。ユーザーが特定の子コンポーネントの「詳細表示」ボタンをクリックしたとき、親コンポーネントがそのアクションを検知し、選択された顧客の詳細情報を画面の別の領域に表示する必要があります。このような親子間の連携を実現するために、LWCではイベント駆動型の通信メカニズムが提供されています。本記事では、この中核をなすCustomEvent API、特にその伝播を制御する`bubbles``composed`プロパティに焦点を当て、Salesforce開発者として堅牢でスケーラブルなコンポーネントを構築するための知識を深掘りします。

原理説明

LWCにおけるコンポーネント間の通信は、標準のWeb標準であるDOM Eventsに基づいています。具体的には、子コンポーネントがイベントをディスパッチ(発行)し、親コンポーネントがそのイベントをリッスン(待機)して捕捉します。この仕組みの中心となるのが`CustomEvent`インターフェースです。

CustomEventの基本

子コンポーネントから親コンポーネントに情報を伝達するには、以下の2つのステップが必要です。

  1. イベントの作成とディスパッチ: 子コンポーネントは `CustomEvent` のインスタンスを作成し、`this.dispatchEvent()` メソッドを使ってイベントを発行します。データを渡したい場合は、`detail` プロパティに格納します。
  2. イベントのハンドリング: 親コンポーネントは、HTMLマークアップ内で子コンポーネントのタグに `on` という形式でイベントリスナーを宣言的に設定し、対応するJavaScriptメソッドでイベントを処理します。

しかし、LWCのコンポーネントはShadow DOM (シャドウDOM)という技術によってカプセル化されています。Shadow DOMは、コンポーネントのDOMツリーを外部のDOMから隔離し、スタイルや構造の意図しない競合を防ぐための強力な仕組みです。このカプセル化により、イベントがコンポーネントの境界を越える際の挙動が重要になります。ここで登場するのが `bubbles` と `composed` プロパティです。

イベント伝播を制御する `bubbles` と `composed`

`CustomEvent` を作成する際、コンストラクタの第2引数でこれらのプロパティを設定できます。

const myEvent = new CustomEvent('myevent', {
  detail: { message: 'Hello from child!' },
  bubbles: true,
  composed: false
});
this.dispatchEvent(myEvent);
  • `bubbles` (ブール値): イベントがDOMツリーを上方向(ターゲットからルートへ)に伝播(バブリング)するかどうかを決定します。
    • `false` (デフォルト): イベントは発行されたコンポーネントの外部には伝播せず、直接の親コンポーネントがテンプレートで宣言的にリッスンしている場合のみ捕捉できます。
    • `true`: イベントはDOMツリーを駆け上がり、祖先コンポーネントもイベントを捕捉できるようになります。
  • `composed` (ブール値): イベントがShadow DOMの境界を越えて伝播できるかどうかを決定します。
    • `false` (デフォルト): イベントは、それが発行されたShadow DOMのツリー内に留まります。
    • `true`: イベントはShadow DOMの境界を越えて、Light DOM(標準のDOM)に存在する祖先要素にまで伝播できます。

これらの組み合わせによって、イベントの到達範囲が大きく変わります。

  • `bubbles: false`, `composed: false` (デフォルト): 最も厳格な設定。イベントはShadow DOMの境界を越えず、バブリングもしません。親コンポーネントがテンプレート上で直接リスナーを設定している場合にのみ機能します。これは、コンポーネントのAPIを明確に定義するための推奨されるデフォルト設定です。
  • `bubbles: true`, `composed: false`: イベントはShadow DOM内でバブリングしますが、境界を越えることはありません。これは、同じShadow DOM内にネストされたコンポーネント階層内でイベントを伝えたい場合に役立ちますが、LWCではあまり一般的なケースではありません。
  • `bubbles: true`, `composed: true`: 最も広範囲に伝播する設定。イベントはShadow DOMの境界を越え、DOMツリーのルートまでバブリングします。これは、アプリケーション全体に影響を与えるようなグローバルなイベントや、コンポーネント階層が非常に深い場合に便利ですが、意図しない副作用を引き起こす可能性があるため、慎重に使用する必要があります。

ほとんどのLWC開発シナリオでは、デフォルトの `bubbles: false, composed: false` を使用し、親が子コンポーネントのイベントを明示的にハンドルすることがベストプラクティスとされています。

示例コード

ここでは、子コンポーネント(`c-contact-list-item`)がイベントをディスパッチし、親コンポーネント(`c-event-bubbling`)がそれをハンドルする公式ドキュメントの例を見てみましょう。

子コンポーネント: `contactListItem.html`

このテンプレートは、クリック可能な `` タグを持ち、クリックされると `selectHandler` メソッドを呼び出します。

<template>
    <li>
        <a href="#" onclick={selectHandler}>{contact.Name}</a>
    </li>
</template>

子コンポーネント: `contactListItem.js`

`selectHandler` メソッドは `select` という名前の `CustomEvent` を作成し、ディスパッチします。このイベントには、選択された連絡先のIDが `detail` プロパティ経由で含まれています。ここでは `bubbles: true` と `composed: true` を設定し、イベントがコンポーネント階層を自由に伝播できるようにしています。

import { LightningElement, api } from 'lwc';

export default class ContactListItem extends LightningElement {
    @api contact;

    selectHandler(event) {
        // Prevents the anchor element from navigating to a URL.
        event.preventDefault();

        // Creates the event with the contact ID payload.
        // bubbles: true と composed: trueにより、このイベントはShadow DOMの境界を越えて
        // DOMツリーを上へバブリングします。
        const selectedEvent = new CustomEvent('select', {
            detail: this.contact.Id,
            bubbles: true,
            composed: true
        });

        // Dispatches the event.
        // イベントを発行します。
        this.dispatchEvent(selectedEvent);
    }
}

親コンポーネント: `eventBubbling.html`

親コンポーネントのテンプレートは、`div` 要素で子コンポーネントのリストをラップしています。イベントリスナー `onselect` は、子コンポーネント自身ではなく、この親の `div` 要素に設定されています。これは `bubbles` が `true` であるために可能な実装です。イベントが子から `div` までバブリングしてくるのを待って捕捉します。

<template>
    <lightning-card title="EventBubbling" icon-name="custom:custom9">
        <div class="slds-m-around_medium" onselect={handleSelect}>
            <template for:each={contacts} for:item="contact">
                <c-contact-list-item
                    key={contact.Id}
                    contact={contact}
                ></c-contact-list-item>
            </template>
        </div>
        <p class="slds-m-around_medium">Selected Contact ID: {selectedContactId}</p>
    </lightning-card>
</template>

親コンポーネント: `eventBubbling.js`

`handleSelect` メソッドは、イベントを捕捉し、`event.detail` から連絡先IDを取得して、`selectedContactId` プロパティを更新します。これにより、UIがリアクティブに更新され、選択されたIDが表示されます。

import { LightningElement, wire } from 'lwc';
import getContactList from '@salesforce/apex/ContactController.getContactList';

export default class EventBubbling extends LightningElement {
    selectedContactId;
    contacts;
    error;

    @wire(getContactList)
    wiredContacts({ error, data }) {
        if (data) {
            this.contacts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.contacts = undefined;
        }
    }

    // イベントがdiv要素までバブリングしてきたときにこのハンドラが実行されます。
    handleSelect(event) {
        // event.detailには、子コンポーネントが設定したペイロード(連絡先ID)が含まれています。
        this.selectedContactId = event.detail;
    }
}

この例は、`bubbles` と `composed` を `true` に設定することで、イベントを直接の子要素だけでなく、より上位のコンテナ要素で一元的に処理できることを示しています。これは、イベントの委任(Event Delegation)パターンを実装する際に非常に強力です。

注意事項

イベント名の命名規則

イベント名は常に小文字で記述する必要があります。HTML属性は大文字と小文字を区別しないため、LWCはイベントリスナーを小文字のイベント名にマッピングします。例えば、`ready` というイベントをディスパッチした場合、親のテンプレートでは `onready` としてリスニングします。キャメルケース(例:`myEvent`)は使用しないでください。

データペイロード (`detail`)

`detail` プロパティには、プリミティブ値(文字列、数値など)またはJavaScriptオブジェクトを渡すことができます。しかし、注意点として、LWCはコンポーネント間で渡されるオブジェクトを不変性を確保するために読み取り専用のプロキシでラップします。子コンポーネントが渡したオブジェクトを、親コンポーネントが直接変更することはできません。もし変更が必要な場合は、親コンポーネント側でオブジェクトの浅いコピーまたは深いコピーを作成してから操作する必要があります。

// 親コンポーネントのハンドラ内
handleNotification(event) {
    // 受信したオブジェクトのコピーを作成して変更する
    const receivedData = { ...event.detail };
    receivedData.status = 'processed';
    // ...
}

パフォーマンスに関する考慮事項

イベント、特にバブリングするイベントを頻繁に発行すると、アプリケーションのパフォーマンスに影響を与える可能性があります。イベントがDOMツリーを上るたびに、各要素でリスナーの存在がチェックされるためです。高頻度で状態を同期する必要がある場合は、イベントを連発するのではなく、共有のJavaScriptサービスクラスや、より大規模で疎結合な通信が必要な場合はLightning Message Service (LMS) (ライトニングメッセージサービス)の使用を検討してください。

まとめとベストプラクティス

LWCにおけるイベントベースの通信は、コンポーネントの再利用性とカプセル化を維持しながら、インタラクティブなアプリケーションを構築するための基本です。`CustomEvent` とその `bubbles`、`composed` プロパティを正しく理解し、使い分けることが高品質なLWC開発の鍵となります。

ベストプラクティス

  1. デフォルトを尊重する: イベントを作成する際は、原則として `bubbles: false` および `composed: false` を使用します。これにより、イベントの影響範囲が最小限に抑えられ、コンポーネントのAPIが明確になります。親コンポーネントは、子コンポーネントのテンプレートタグに明示的にリスナーを設定する必要があります。
  2. イベントは公開APIと見なす: コンポーネントがディスパッチするイベントは、そのコンポーネントの公開APIの一部です。イベント名、`detail` オブジェクトの構造は、明確に定義し、ドキュメント化することが望ましいです。
  3. バブリングは慎重に: `bubbles: true` と `composed: true` は、イベントの委任パターンを実装する場合や、コンポーネントが自身のコンテキストを知らずに汎用的なアクションを通知する必要がある場合にのみ使用します。乱用は、予期せぬ場所でイベントが捕捉され、デバッグが困難になる原因となります。
  4. 適切な通信方法を選択する:
    • 子から親へ: `CustomEvent` を使用します。
    • 親から子へ: 公開プロパティ (`@api` デコレータ) または公開メソッド (`@api` デコレータ) を使用します。
    • 直接の親子関係にないコンポーネント間: 共通の祖先コンポーネントを介してイベントを中継するか、より疎結合な通信が求められる場合は Lightning Message Service (LMS) を使用します。

これらの原則に従うことで、Salesforce開発者は、保守性が高く、予測可能で、スケーラブルなLightning Web Componentsを構築することができるでしょう。

コメント