Lightning Web Component 間の通信の基礎

# LWCのコンポーネント間通信
LWCのコンポーネント間の通信は主に下記の3つがあります。

1. カスタムイベント(子→親)
1. パブリックプロパティ・パブリックメソッド(親→子)
1. Lightning Message Service

![コンポーネント間通信.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/3fd6bab8-9dda-a04e-db91-971a8645d665.png)

## ①子から親への通信
### カスタムイベント
子コンポーネントから親コンポーネントに情報を伝えるためには、カスタムイベントを使用します。
子コンポーネントでカスタムイベントをディスパッチし、親コンポーネントのイベントハンドラーで処理を行います。

下記の例では、子コンポーネントをクリックしたときにカスタムイベント「select」をディスパッチして、親コンポーネントに「name」の値を渡し、親コンポーネントで画面表示しています。

![カスタムイベント.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/959a667b-744a-0a6a-7388-7d8892cb84b0.gif)

#### 子コンポーネント
```html:child.html
<template>
    <!-- ↓子コンポーネントをクリックしたときにhandleClickメソッドを実行します。 -->
    <a onclick={handleClick}>
        <div class="sample">
            <span>{name}</span>
        </div>
    </a>
</template>
```

```javascript:child.js
import { LightningElement, api } from 'lwc';
export default class ChildComponent extends LightningElement {
    @api name = '';

    // handleClickでは、「select」という名のカスタムイベントを作成し、値として「this.name」を設定しています。
    // dispatchEventメソッドでカスタムイベントをディスパッチし、親コンポーネントに伝達します。
    handleClick() {
        const selectEvent = new CustomEvent(
            'select',
            { detail: this.name }
        );
        this.dispatchEvent(selectEvent);
    }
}
```

:::note
上記では親の親にイベントは伝播しません。伝播させるためには、`Event.bubbles`、`Event.composed`を設定します。

```javascript:補足
// 親のDOMにイベントを伝えない ＋ 親LWCにイベントを伝えない（デフォルト）
new CustomEvent('sample', { detail: 'sample', bubbles: false, composed: false })

// 親のDOMにイベントを伝える ＋ 親LWCにイベントを伝えない
new CustomEvent('sample', { detail: 'sample', bubbles: true, composed: false })

// 下記の設定はサポートされない
// new CustomEvent('sample', { detail: 'sample', bubbles: false, composed: true })

// 親のDOMにイベントを伝える ＋ 親LWCにイベントを伝える
new CustomEvent('sample', { detail: 'sample', bubbles: true, composed: true })
```

- [イベント伝達の設定 | Lightning Web Components 開発者ガイド](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/events-propagation.html)
- [How Events Bubble in Lightning Web Components | Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/08/how-events-bubble-in-lightning-web-components)
:::

#### 親コンポーネント
```html:parent.html
<template>
    <lightning-card title="Parent" icon-name="custom:custom53">
        <p class="slds-align_absolute-center slds-text-heading_large">
            <span>選択した子コンポーネント:  {selected}</span>
        </p>
        <div class="slds-m-top_xx-large slds-align_absolute-center">
            <!-- ↓on「カスタムイベント名」で、カスタムイベントのリスナーを宣言します。
                 ここでは「handleSelect」がリスナーとして宣言されています。 -->
            <c-child
                name="Child A"
                onselect={handleSelect}
                class="slds-m-horizontal_small"></c-child-component>
            <c-child
                name="Child B"
                onselect={handleSelect}
                class="slds-m-horizontal_small"></c-child-component>
        </div>
    </lightning-card>
</template>
```

```javascript:parent.js
import { LightningElement } from 'lwc';
export default class ParentComponent extends LightningElement {
    selected = '';

    // handleSelectメソッドでカスタムイベントを処理します。
    // 子コンポーネントで設定した値は「event.detail」で取り出すことができます。
    handleSelect(event) {
        this.selected = event.detail;
    }
}
```

:::note
イベントの伝播を止めるには`event.stopPropagation()`を、ブラウザ標準のイベント処理を止めるには`event.preventDefault()`を使用します。

- [Event: stopPropagation() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Event/stopPropagation)
- [Event: preventDefault() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault)
:::

## ②親から子への通信
親から子に値を渡す方法は、パブリックプロパティとパブリックメソッドの二種類あります。
パブリックメソッドを使用した方法は若干複雑になります。
[公式開発者ブログ](https://developer.salesforce.com/blogs/2020/10/step-up-your-lwc-skills-part-1.html)では「パブリックプロパティを常に優先する」とあるので、パブリックプロパティが使える場合はパブリックプロパティを使いましょう。

下記の例では、親コンポーネントのコンボボックスで色を選択し、親コンポーネントから子のパブリックプロパティまたはパブリックメソッドを使用して、子コンポーネントの背景色を変更します。

![プロパティ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/69556f81-f923-bc60-1e42-1607e33d4e5b.gif)

### パブリックプロパティ
#### 親コンポーネント
属性名はパブリックプロパティのケバブケースになります。`@api colorName`は`color-name='xxx'`となります。

```html:parent.html
<template>
    <lightning-card title="Parent" icon-name="custom:custom53">
        <lightning-layout>
            <lightning-layout-item padding="around-small">
                <!-- ↓コンボボックスで色を選択します。 -->
                <lightning-combobox
                    name="color"
                    label="Color"
                    value={color}
                    options={options}
                    onchange={handleChange}
                ></lightning-combobox>
            </lightning-layout-item>
            <lightning-layout-item padding="around-small">
                <!-- ↓子コンポーネントのパブリックプロパティ「color」に値を渡します。 -->
                <c-child color-name={color}></c-child>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>
```

```javascript:parent.js
import { LightningElement } from 'lwc';
export default class Parent extends LightningElement {
    // ↓HTMLで子コンポーネントに渡している値です。
    color = 'blue';

    get options() {
        return [
            { label: 'BLUE', value: 'blue' },
            { label: 'RED', value: 'red' },
            { label: 'GREEN', value: 'green' },
        ];
    }

    handleChange(event) {
        this.color = event.detail.value;
    }
}
```

#### 子コンポーネント
```html:child.html
<template>
    <!-- 親から渡された値を使用して、スタイルを設定しています。 -->
    <div class="sample" style={backgroundStyle}>
        <span>Child {colorName}</span>
    </div>
</template>
```

```javascript:child.js
import { LightningElement, api } from 'lwc';
export default class Child extends LightningElement {
    // ↓パブリックプロパティです。@apiデコレーターを使用して宣言します。
    // @apiをつけることで親コンポーネントからアクセス可能になります。
    @api colorName;

    get backgroundStyle() {
        return `background-color:${this.colorName}`;
    }
}
```

### パブリックメソッド
パブリックプロパティと同じことをパブリックメソッドで実装すると下記のようになります。
#### 親コンポーネント
```html:parent.html
<template>
    <lightning-card title="Parent" icon-name="custom:custom53">
        <lightning-layout>
            <lightning-layout-item padding="around-small">
                <lightning-combobox
                    name="color"
                    label="Color"
                    value={color}
                    options={options}
                    onchange={handleChange}
                ></lightning-combobox>
            </lightning-layout-item>
            <lightning-layout-item padding="around-small">
                <!-- ↓プロパティは使用しません。 -->
                <c-child></c-child>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>
```

```javascript:parent.js
import { LightningElement } from 'lwc';
export default class Parent extends LightningElement {
    color = 'blue';

    // ↓下記がパブリックメソッドを使用している箇所になります。
    // querySelectorメソッドで子コンポーネントを取得する必要があります。
    // 同じ子コンポーネントが複数ある場合はquerySelectorAllメソッドを使用します。
    renderedCallback() {
        this.template.querySelector('c-child').setColor(this.color);
    }

    handleChange(event) {
        this.color = event.detail.value;
    }

    get options() {
        return [
            { label: 'BLUE', value: 'blue' },
            { label: 'RED', value: 'red' },
            { label: 'GREEN', value: 'green' },
        ];
    }
}
```

#### 子コンポーネント
```javascript:child.js
import { LightningElement, api } from 'lwc';
export default class Child extends LightningElement {
    color;

    // ↓パブリックメソッドを宣言している箇所になります。
    @api
    setColor(value) {
        this.color = value;
    }

    get backgroundStyle() {
        return `background-color:${this.color}`;
    }
}
```

## ③親子関係でないコンポーネントへの通信
### Lightning Message Service
親子関係にないコンポーネントへの通信にはLightning Message Service(LMS)を使用します。
LMSはLWCだけでなく、VisualforceやAuraにも対応しています。
LMSを使用するためにはまずメッセージチャネルというメタデータを作成する必要があります。
情報を受け取る側のコンポーネント(Subscriber)は、メッセージチャネルをインポートして、購読(Subscribe)する必要があります。
情報を渡す方側のコンポーネント(Publisher)は、同様にメッセージチャネルをインポートして、送信したいデータを公開(Publish)します。
公開を行ったら、購読中の状態にあるコンポーネントすべてに一度にデータが送信されます。

![LMS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/d785690a-9a0f-8d6f-b8e5-8f4b094b3de1.png)

下記の例では、Publisherのコンボボックスで取引先を選択したとき、取引先IDをSampleMessageChannel__cに公開(Publish)します。
SubscriberはSampleMessageChannel__cを購読(Subscribe)しています。
SampleMessageChannel__cから取引先IDを取得したSubscriberは、lightning-record-formに取引先IDを渡し、詳細情報を表示しています。

![LMS.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/4986b92b-fc03-fe67-8cb6-c6d79f71031d.gif)

## MessageChannel
LMSを使用するには、MessageChannelのメタデータをデプロイする必要があります。
UIから作成することができないため、SalesforceCLIを使ってデプロイします。

```xml:SampleMessageChannel.messageChannel-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>サンプルメッセージサービス</description>
    <isExposed>true</isExposed>
    <lightningMessageFields>
        <description>AccountIdを格納</description>
        <fieldName>recordId</fieldName>
    </lightningMessageFields>
    <masterLabel>SampleMessageChannel</masterLabel>
</LightningMessageChannel>
```

```terminal:MessageChannelをデプロイ
$ sfdx force:source:deploy -p force-app/main/default/messageChannels/SampleMessageChannel.messageChannel-meta.xml
```


## Publisher
データを送信する側のLWCです。

```javascript:publisher.js
import { LightningElement, wire } from 'lwc';

// LMSで使用する関数とカスタムMessageChannelをインポートします。
import { publish, MessageContext } from 'lightning/messageService';
import MC from "@salesforce/messageChannel/SampleMessageChannel__c";

export default class Parent extends LightningElement {
    accountId = '';

    // ワイヤーサービスを使用してMessageContextオブジェクトを作成します。
    @wire(MessageContext)
    messageContext;

    handleChange(event) {
        this.accountId = event.detail.value;
        this.sendMessageService(this.accountId);
    }

    sendMessageService(recordId) {
        // publishメソッドで取引先IDをSampleMessageChannelに公開します。
        const payload = {recordId: recordId};
        publish(this.messageContext, MC, payload);
    }

    get options() {
        return [
            { label: 'select one', value: '' },
            { label: 'sForce', value: '0012x000009UhXQAA0' },
            { label: 'United Oil & Gas Corp.', value: '0012x000009UhXKAA0' },
            { label: 'Edge Communications', value: '0012x000009UhXFAA0' },
        ];
    }
}
```

## Subscriber
データを受信する側のLWCです。

```javascript:subscriber.js
import { LightningElement, api, wire } from 'lwc';

// LMSで使用する関数とカスタムMessageChannelをインポートします。
import { subscribe, unsubscribe, MessageContext, APPLICATION_SCOPE } from 'lightning/messageService';
import MC from "@salesforce/messageChannel/SampleMessageChannel__c";

export default class Subscriber extends LightningElement {
    @api recordId;

    // subscribe()の戻り値を格納する変数です。
    subscription;

    // ワイヤーサービスを使用してMessageContextオブジェクトを作成します。
    @wire(MessageContext)
    messageContext;

    // 購読状態にないとき、subscribe()を実行し、SampleMessageChannelを購読します。
    connectedCallback() {
        if (this.subscription || this.recordId) {
            return;
        }
        this.subscribeToMessageChannel();
    }

    // unsubscribeメソッドで購読状態を解除できます。
    disconnectedCallback() {
        unsubscribe(this.subscription);
        this.subscription = null;
    }

    subscribeToMessageChannel() {
        this.subscription = subscribe(
            this.messageContext,
            MC,
            // ↓メッセージを受信したときのコールバック関数です。
            (message) => { this.recordId = message.recordId },
            // ↓非必須の引数です。APPLICATION_SCOPEにすることで、LWCが非アクティブでもメッセージを受信できます。
            { scope: APPLICATION_SCOPE }
        );
    }
}
```


## 参考
- 子から親への通信
    - [イベントを使用した通信 | Developer Guide](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/lwc.events)
    - [イベント伝達の設定 | Lightning Web Components 開発者ガイド](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/events-propagation.html)
    - [How Events Bubble in Lightning Web Components | Salesforce Developers Blog](https://developer.salesforce.com/blogs/2021/08/how-events-bubble-in-lightning-web-components)
    - [Event: stopPropagation() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Event/stopPropagation)
    - [Event: preventDefault() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault)
- 親から子への通信
    - [子コンポーネントのプロパティの設定 | Developer Guide](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/lwc.create_components_data_binding)
    - [子コンポーネントでのメソッドのコール | Developer Guide](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/lwc.create_javascript_methods)
    - [renderedCallback() | Developer Guide](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/lwc.create_lifecycle_hooks_rendered)
    - [Step Up Your LWC Skills – Part 1](https://developer.salesforce.com/blogs/2020/10/step-up-your-lwc-skills-part-1.html) ー 「Favor public properties over public methods」
- 親子関係にないコンポーネントへの通信
    - [Lightning Message Service を使用した DOM 間の通信 | Developer Guide](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/use_message_channel)