LWC コンポーネント間通信の完全ガイド:親子間のデータ連携をマスターする

執筆者:Salesforce 開発者


背景と適用シナリオ

Salesforce のフロントエンド開発において、Lightning Web Components (LWC、ライトニングWebコンポーネント) は、モダンな Web 標準に基づいて構築された、パフォーマンスと再利用性に優れたフレームワークです。複雑なアプリケーションを構築する際には、単一の巨大なコンポーネントを作成するのではなく、機能を小さなコンポーネントに分割し、それらを組み合わせて UI を構築することがベストプラクティスとされています。このアプローチにより、コードの保守性、可読性、再利用性が飛躍的に向上します。

しかし、コンポーネントを分割すると、新たな課題が生まれます。それは「コンポーネント間でどのようにデータをやり取りし、連携させるか」という問題です。例えば、以下のようなシナリオを考えてみましょう。

  • マスター-詳細ビュー: 取引先リスト(親コンポーネント)から特定の取引先を選択すると、その詳細情報が別のコンポーネント(子コンポーネント)に表示される。
  • フォーム入力: ユーザーが入力フォーム(子コンポーネント)でデータを入力し、「保存」ボタンをクリックすると、そのデータが親コンポーネントに渡され、Apex コントローラーを呼び出して保存処理が実行される。
  • 動的なフィルタリング: 検索条件を入力するフィルターコンポーネント(子コンポーネント)があり、ユーザーが条件を変更するたびに、その条件が親コンポーネントに通知され、表示されるデータリストが更新される。

これらのシナリオを実現するためには、コンポーネント間の効果的な通信メカニズムが不可欠です。本記事では、LWC 開発者として、最も基本的かつ重要な通信パターンである親子間のデータ連携に焦点を当て、その原理と実装方法を公式ドキュメントのコード例を交えながら詳細に解説します。

原理の説明

LWC における親子間の通信は、データの流れる方向によって使用する技術が異なります。この方向性を理解することが、堅牢なコンポーネント設計の第一歩です。

1. 親から子への通信:Public Property (@api デコレーター)

親コンポーネントから子コンポーネントへデータを渡す場合、子コンポーネントのプロパティを公開 (Public) する方法が用いられます。これを実現するのが `@api` decorator (デコレーター) です。

子コンポーネントの JavaScript ファイル内で、外部からアクセスさせたいプロパティやメソッドの前に `@api` を付与します。これにより、そのプロパティは Public API の一部となり、親コンポーネントの HTML マークアップから属性として値を設定できるようになります。

データフローは以下のようになります。

  1. 子コンポーネントが `@api` でプロパティを公開する。
  2. 親コンポーネントが HTML テンプレート内で子コンポーネントを呼び出し、公開されたプロパティに対応する属性(ケバブケース形式)に値を渡す。
  3. 子コンポーネントは渡された値を受け取り、自身のテンプレートやロジック内で利用する。

この方法は、親が子の状態を制御するための、一方向のデータバインディングです。親のデータが変更されると、子のプロパティも自動的に更新され、UI が再レンダリングされます。

2. 子から親への通信:Custom Event (カスタムイベント)

子コンポーネントで発生した出来事(例:ボタンのクリック、データの選択)を親コンポーネントに通知する場合は、CustomEvent を使用します。LWC は標準の DOM Events モデルに従っており、コンポーネントはイベントをディスパッチ(発行)し、その親コンポーネントがそれをリッスン(捕捉)することができます。

データフローは以下のようになります。

  1. 子コンポーネント内で、何らかのユーザーアクションをトリガーとして `CustomEvent` のインスタンスを作成する。データを含めたい場合は `detail` プロパティに格納する。
  2. `this.dispatchEvent()` メソッドを使用して、作成したイベントを発行する。
  3. 親コンポーネントの HTML テンプレート内で、子コンポーネントのタグに `on` という形式のイベントリスナーを宣言し、対応するハンドラーメソッドを指定する。
  4. イベントが発生すると、親コンポーネントのハンドラーメソッドが実行され、イベントの `detail` プロパティからデータを受け取ることができる。

この方法は、子が親に「何か起きた」ことを通知し、データの所有者である親に処理を委ねるための重要なパターンです。コンポーネントのカプセル化を維持し、子が親の実装を直接知る必要がないため、疎結合な設計が実現できます。


示例代码

ここでは、Salesforce 公式ドキュメントに基づいたコード例を用いて、上記の2つの通信パターンを具体的に見ていきましょう。

親から子への通信の例

取引先の詳細情報を表示するシンプルなシナリオを考えます。親コンポーネント `c-contact-list` が子コンポーネント `c-contact-tile` に取引先オブジェクトを渡します。

子コンポーネント: contactTile.js

まず、データを受け取る子コンポーネントです。`@api` デコレーターを使って `contact` プロパティを公開します。

import { LightningElement, api } from 'lwc';

export default class ContactTile extends LightningElement {
    // @api デコレーターを使用して 'contact' プロパティを
    // public プロパティとして公開します。
    // これにより、親コンポーネントからデータを受け取ることができます。
    @api contact;
}

子コンポーネント: contactTile.html

受け取った `contact` オブジェクトのプロパティをテンプレートに表示します。

<template>
    <p>{contact.Name}</p>
    <p>{contact.Title}</p>
</template>

親コンポーネント: contactList.js

親コンポーネントは、表示する取引先データのリストを保持します。

import { LightningElement } from 'lwc';

export default class ContactList extends LightningElement {
    // テンプレートで利用するための取引先レコードの配列
    contacts = [
        {
            Id: '0031700000pJRRSAA4',
            Name: 'Amy Taylor',
            Title: 'VP of Engineering',
        },
        {
            Id: '0031700000pJRRTAA4',
            Name: 'Michael Jones',
            Title: 'VP of Sales',
        },
    ];
}

親コンポーネント: contactList.html

`for:each` ディレクティブで `contacts` 配列をループ処理し、各取引先データを子コンポーネント `c-contact-tile` に渡します。`contact` プロパティ(JS側)は、HTMLでは `contact` 属性(ケバブケースでは `contact` のまま)に対応します。`key` 属性はリストレンダリングのパフォーマンス最適化に必須です。

<template>
    <template for:each={contacts} for:item="contact">
        <!-- 
            子コンポーネント c-contact-tile を呼び出します。
            子コンポーネントの 'contact' プロパティに、現在のループ変数 'contact' を渡します。
            key 属性は、リストの各項目を一意に識別するために必要です。
        -->
        <c-contact-tile 
            key={contact.Id} 
            contact={contact}
        ></c-contact-tile>
    </template>
</template>

この例では、`contactList` が `contacts` 配列の各要素を `contactTile` コンポーネントの `contact` プロパティにバインドしています。これにより、各 `contactTile` は異なる取引先情報を表示できます。

子から親への通信の例

次に、子コンポーネントでのボタンクリックを親に通知するシナリオです。子コンポーネント `c-controls` にあるボタンがクリックされると、カスタムイベントが発行され、親コンポーネント `c-event-simple-demo` がそれを捕捉してカウンターを増減させます。

子コンポーネント: controls.js

このコンポーネントには、加算と減算のボタンがあります。それぞれのクリックイベントで、対応するカスタムイベントを発行します。

import { LightningElement } from 'lwc';

export default class Controls extends LightningElement {
    // '+' ボタンがクリックされたときに呼び出されるハンドラー
    handleAdd() {
        // 'add' という名前のカスタムイベントを発行します。
        this.dispatchEvent(new CustomEvent('add'));
    }

    // '-' ボタンがクリックされたときに呼び出されるハンドラー
    handleSubtract() {
        // 'subtract' という名前のカスタムイベントを発行します。
        this.dispatchEvent(new CustomEvent('subtract'));
    }
}

子コンポーネント: controls.html

<template>
    <lightning-button 
        label="Subtract" 
        onclick={handleSubtract}
    ></lightning-button>
    <lightning-button 
        label="Add" 
        onclick={handleAdd}
    ></lightning-button>
</template>

親コンポーネント: eventSimpleDemo.js

親コンポーネントは、カウンターの値を保持し、子からのイベントを処理するハンドラーメソッドを定義します。

import { LightningElement } from 'lwc';

export default class EventSimpleDemo extends LightningElement {
    counter = 0;

    // 'add' イベントを処理するハンドラー
    handleMultiply(event) {
        // この例ではイベントの detail を使用していませんが、
        // event.detail で子から渡されたデータにアクセスできます。
        this.counter++;
    }

    // 'subtract' イベントを処理するハンドラー
    handleDivide(event) {
        this.counter--;
    }
}

親コンポーネント: eventSimpleDemo.html

子コンポーネントを配置し、`onadd` と `onsubtract` というイベントリスナーを設定します。イベント名は小文字になります。

<template>
    <p>Counter: {counter}</p>
    <!-- 
        子コンポーネント c-controls を呼び出します。
        'onadd' リスナーは、子の 'add' イベントを捕捉し、handleMultiply メソッドを呼び出します。
        'onsubtract' リスナーは、子の 'subtract' イベントを捕捉し、handleDivide メソッドを呼び出します。
    -->
    <c-controls
        onadd={handleMultiply}
        onsubtract={handleDivide}
    ></c-controls>
</template>

この例では、`c-controls` でボタンがクリックされると、`add` または `subtract` イベントが発生します。親の `c-event-simple-demo` はこれらのイベントをリッスンし、`counter` の値を更新して UI に反映させます。

注意事項

プロパティ名と属性名の変換

親から子へデータを渡す際、JavaScript でのプロパティ名(キャメルケース、例: `myProperty`)は、HTML マークアップでは属性名(ケバブケース、例: `my-property`)に変換されます。この命名規則を常に意識することが重要です。簡単な単語の場合は、`contact` -> `contact` のように変化しませんが、複合語の場合は変換が必要です。

CustomEvent の設定

`CustomEvent` を作成する際、第2引数として設定オブジェクトを渡すことができます。特に重要なのは `bubbles` と `composed` プロパティです。

  • `bubbles: true`: イベントが DOM ツリーを上に伝播(バブリング)することを許可します。これにより、直接の親だけでなく、さらに上位の祖先コンポーネントもイベントを捕捉できるようになります。デフォルトは `false` です。
  • `composed: true`: イベントが Shadow DOM の境界を越えて伝播することを許可します。LWC は Shadow DOM を使用しているため、コンポーネントの境界を越えてイベントを伝えたい場合にこの設定が必要になります。デフォルトは `false` です。

通常、コンポーネント間の疎結合を保つため、イベントは直接の親のみがリッスンすることが推奨されるため、デフォルト値のままで問題ないことが多いです。しかし、より複雑なコンポーネント階層でイベントを伝播させる必要がある場合は、これらのプロパティを理解しておく必要があります。

データの不変性 (Immutability)

親から子へオブジェクトや配列を渡した場合、子は受け取ったデータを直接変更すべきではありません。これは、予期せぬ副作用を生み、デバッグを困難にするためです。子がデータを変更する必要がある場合は、親にイベントを送信してデータの変更を依頼するか、受け取ったデータのコピーを作成してそれを変更するようにしてください。

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

本記事では、LWC における最も基本的な通信パターンである親子間のデータ連携について解説しました。

  • 親から子へ: `@api` デコレーターを使用して子のプロパティを公開し、親が HTML 属性を通じてデータを渡します。これは「プロパティは下へ」という原則に従います。
  • 子から親へ: `CustomEvent` を使用して子がイベントを発行し、親がそれをリッスンして処理します。これは「イベントは上へ」という原則に従います。

これらのパターンを適切に使い分けることで、再利用可能で保守性の高いコンポーネントを構築することができます。

ベストプラクティス

  1. 責務の分離: 親コンポーネントはデータの管理とビジネスロジックに責任を持ち、子コンポーネントはデータの表示やユーザーインタラクションの処理に特化させるように設計します。
  2. 疎結合の維持: 子コンポーネントは親コンポーネントの実装に依存しないようにします。イベントを通じて通信することで、親は子がどのように実装されているかを知る必要がなくなり、逆もまた然りです。
  3. 明確な API 設計: `@api` で公開するプロパティやメソッドは、コンポーネントの公開 API となります。命名は分かりやすく、意図が明確に伝わるように心がけましょう。
  4. イベントの命名規則: イベント名は、`onchange`, `onselect`, `onclose` のように、何が起きたかを表す動詞を含めると分かりやすくなります。
  5. より複雑な通信には: 親子関係にないコンポーネント間の通信が必要な場合は、Lightning Message Service (LMS) や、Pub-Sub パターンを実装した共有 JavaScript モジュールを利用することを検討してください。これにより、アプリケーション全体のどこからでもメッセージを送受信できます。

LWC の親子間通信は、コンポーネントベース開発の基礎です。これらの原則とパターンをマスターすることで、Salesforce プラットフォーム上でより高度でスケーラブルなアプリケーションを構築する道が開かれるでしょう。

コメント