Lightning Web Component Communication Patterns in Salesforce

2025-12-03

Lightning Web Components (LWC) are modular, reusable pieces of UI. In real applications, components need to communicate with each other—sharing data, triggering actions, and coordinating behavior.

Understanding the different communication patterns and when to use each is crucial for building maintainable, scalable LWC applications.


Communication Patterns Overview

LWC provides several ways for components to communicate:

  1. Parent-to-Child: Using properties and method calls
  2. Child-to-Parent: Using events
  3. Sibling Components: Using events through a common parent
  4. Unrelated Components: Using Lightning Message Service (pub/sub)
  5. Component-to-Apex: Using wire service and imperative calls

Each pattern has specific use cases and trade-offs.


1. Parent-to-Child Communication

Using Properties (@api)

The simplest and most common pattern: pass data from parent to child using public properties.

Child Component (childComponent.js):

import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {
    @api message;
    @api userId;
    @api config;
    
    // Computed property based on @api
    get displayMessage() {
        return this.message ? `Hello, ${this.message}` : 'No message';
    }
}

Child Component Template (childComponent.html):

<template>
    <div class="child">
        <p>{displayMessage}</p>
        <p>User ID: {userId}</p>
    </div>
</template>

Parent Component (parentComponent.js):

import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    message = 'World';
    userId = '001xx000003DGbQAAW';
    config = {
        showDetails: true,
        theme: 'light'
    };
}

Parent Component Template (parentComponent.html):

<template>
    <c-child-component 
        message={message}
        user-id={userId}
        config={config}>
    </c-child-component>
</template>

Key Points:

  • Properties are reactive: When parent updates the property, child automatically re-renders
  • Use @api to expose properties to parent components
  • Property names in HTML use kebab-case (user-id), JavaScript uses camelCase (userId)
  • Properties are one-way data flow: Parent → Child only

Calling Child Methods

Parents can call public methods on child components using template references.

Child Component (childComponent.js):

import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {
    @api
    refreshData() {
        // Refresh logic
        this.loadData();
    }
    
    @api
    getSelectedItems() {
        return this.selectedItems;
    }
    
    loadData() {
        // Implementation
    }
}

Parent Component Template (parentComponent.html):

<template>
    <c-child-component 
        message={message}
        lwc:ref="childRef">
    </c-child-component>
    
    <lightning-button 
        label="Refresh Child"
        onclick={handleRefresh}>
    </lightning-button>
</template>

Parent Component (parentComponent.js):

import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    handleRefresh() {
        // Call child method using template reference
        const childComponent = this.template.querySelector('c-child-component');
        childComponent.refreshData();
    }
    
    handleGetSelection() {
        const childComponent = this.template.querySelector('c-child-component');
        const selected = childComponent.getSelectedItems();
        console.log('Selected items:', selected);
    }
}

Key Points:

  • Use @api to expose methods to parent
  • Access child using querySelector or template references
  • Methods can return values to parent
  • Use sparingly—prefer events for child-to-parent communication

2. Child-to-Parent Communication

Using Custom Events

Children communicate with parents by dispatching custom events.

Child Component (childComponent.js):

import { LightningElement } from 'lwc';

export default class ChildComponent extends LightningElement {
    handleButtonClick() {
        const selectedEvent = new CustomEvent('itemselected', {
            detail: {
                itemId: '12345',
                itemName: 'Selected Item',
                timestamp: Date.now()
            },
            bubbles: true,  // Event bubbles up to parent
            composed: true // Event crosses shadow DOM boundary
        });
        
        this.dispatchEvent(selectedEvent);
    }
    
    handleInputChange(event) {
        const inputValue = event.target.value;
        
        const changeEvent = new CustomEvent('valuechange', {
            detail: { value: inputValue },
            bubbles: true,
            composed: true
        });
        
        this.dispatchEvent(changeEvent);
    }
}

Child Component Template (childComponent.html):

<template>
    <lightning-button 
        label="Select Item"
        onclick={handleButtonClick}>
    </lightning-button>
    
    <lightning-input 
        label="Enter Value"
        onchange={handleInputChange}>
    </lightning-input>
</template>

Parent Component Template (parentComponent.html):

<template>
    <c-child-component 
        onitemselected={handleItemSelected}
        onvaluechange={handleValueChange}>
    </c-child-component>
    
    <div if:true={selectedItem}>
        Selected: {selectedItem.name}
    </div>
</template>

Parent Component (parentComponent.js):

import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    selectedItem = null;
    currentValue = '';
    
    handleItemSelected(event) {
        // Access event data via event.detail
        this.selectedItem = {
            id: event.detail.itemId,
            name: event.detail.itemName
        };
    }
    
    handleValueChange(event) {
        this.currentValue = event.detail.value;
    }
}

Key Points:

  • Use CustomEvent for child-to-parent communication
  • Pass data via detail property
  • Set bubbles: true and composed: true for events to reach parent
  • Event names in HTML use lowercase (onitemselected), JavaScript uses camelCase (itemselected)

Event Naming Convention

Follow Salesforce’s naming convention:

// ✅ GOOD: Use lowercase with hyphens
const event = new CustomEvent('itemselected', { ... });
const event = new CustomEvent('valuechange', { ... });

// ❌ BAD: Don't use camelCase in event names
const event = new CustomEvent('itemSelected', { ... }); // Avoid

3. Sibling Component Communication

Siblings communicate through their common parent using events.

Architecture:

Parent Component
├── Child Component A (dispatches event)
└── Child Component B (receives via parent)

Child Component A (childA.js):

import { LightningElement } from 'lwc';

export default class ChildA extends LightningElement {
    handleAction() {
        const event = new CustomEvent('datachanged', {
            detail: { data: 'new data' },
            bubbles: true,
            composed: true
        });
        this.dispatchEvent(event);
    }
}

Parent Component (parentComponent.html):

<template>
    <c-child-a ondatachanged={handleDataChanged}></c-child-a>
    <c-child-b data={sharedData}></c-child-b>
</template>

Parent Component (parentComponent.js):

import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    sharedData = null;
    
    handleDataChanged(event) {
        // Receive from Child A
        this.sharedData = event.detail.data;
        // Data automatically flows to Child B via property binding
    }
}

Child Component B (childB.js):

import { LightningElement, api } from 'lwc';

export default class ChildB extends LightningElement {
    @api data; // Receives data from parent
    
    // React to data changes
    renderedCallback() {
        if (this.data) {
            // Process data
        }
    }
}

4. Unrelated Components: Lightning Message Service

For components that aren’t in a parent-child relationship (e.g., components in different parts of the page, or in different tabs), use Lightning Message Service (pub/sub pattern).

Setting Up Message Channel

Create a message channel file: messageChannels/NotificationMessage.messageChannel-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningMessageChannel xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>Channel for notifications</description>
    <isExposed>true</isExposed>
    <lightningMessageFields>
        <description>Message text</description>
        <fieldName>message</fieldName>
    </lightningMessageFields>
    <lightningMessageFields>
        <description>Message type</description>
        <fieldName>type</fieldName>
    </lightningMessageFields>
</LightningMessageChannel>

Publishing Messages (Sender Component)

import { LightningElement } from 'lwc';
import { publish, MessageContext } from 'lightning/messageService';
import NOTIFICATION_MESSAGE from '@salesforce/messageChannel/NotificationMessage__c';

export default class SenderComponent extends LightningElement {
    messageContext;
    
    connectedCallback() {
        // Get message context (usually from parent or app)
        // In practice, you'd inject this via @wire or get it from parent
    }
    
    handleSendMessage() {
        const payload = {
            message: 'Hello from Sender!',
            type: 'info'
        };
        
        publish(this.messageContext, NOTIFICATION_MESSAGE, payload);
    }
}

Subscribing to Messages (Receiver Component)

import { LightningElement } from 'lwc';
import { subscribe, MessageContext, unsubscribe } from 'lightning/messageService';
import NOTIFICATION_MESSAGE from '@salesforce/messageChannel/NotificationMessage__c';

export default class ReceiverComponent extends LightningElement {
    subscription = null;
    receivedMessage = null;
    
    @wire(MessageContext)
    messageContext;
    
    connectedCallback() {
        this.subscribeToMessageChannel();
    }
    
    subscribeToMessageChannel() {
        if (!this.subscription) {
            this.subscription = subscribe(
                this.messageContext,
                NOTIFICATION_MESSAGE,
                (message) => this.handleMessage(message)
            );
        }
    }
    
    handleMessage(message) {
        this.receivedMessage = message;
        // Process message
        console.log('Received:', message.message);
    }
    
    disconnectedCallback() {
        unsubscribe(this.subscription);
        this.subscription = null;
    }
}

Key Points:

  • Use for components that aren’t in parent-child relationship
  • Requires message channel definition
  • Always unsubscribe in disconnectedCallback to prevent memory leaks
  • Works across different parts of the application, even in different tabs (with proper setup)

5. Component-to-Apex Communication

Using Wire Service (Reactive)

Wire service automatically updates when data changes.

Apex Class (AccountController.cls):

public with sharing class AccountController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        return [SELECT Id, Name, Industry FROM Account LIMIT 10];
    }
    
    @AuraEnabled(cacheable=true)
    public static Account getAccount(Id accountId) {
        return [SELECT Id, Name, Industry FROM Account WHERE Id = :accountId];
    }
}

LWC Component (accountList.js):

import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountList extends LightningElement {
    accounts = [];
    error;
    
    @wire(getAccounts)
    wiredAccounts({ error, data }) {
        if (data) {
            this.accounts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.accounts = [];
        }
    }
}

Using Imperative Calls (On-Demand)

For non-cacheable methods or when you need to call Apex based on user action.

LWC Component (accountDetail.js):

import { LightningElement } from 'lwc';
import getAccount from '@salesforce/apex/AccountController.getAccount';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class AccountDetail extends LightningElement {
    accountId = '001xx000003DGbQAAW';
    account = null;
    loading = false;
    
    async loadAccount() {
        this.loading = true;
        try {
            this.account = await getAccount({ accountId: this.accountId });
        } catch (error) {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error',
                    message: error.body.message,
                    variant: 'error'
                })
            );
        } finally {
            this.loading = false;
        }
    }
}

Best Practices and Patterns

1. Choose the Right Pattern

  • Parent-to-Child: Use @api properties for simple data passing
  • Child-to-Parent: Use custom events for actions and data updates
  • Siblings: Route through common parent
  • Unrelated: Use Lightning Message Service
  • Apex Data: Use @wire for reactive, imperative calls for on-demand

2. Keep Components Focused

Each component should have a single responsibility. If communication becomes complex, consider:

  • Breaking components into smaller pieces
  • Using a state management pattern
  • Creating a service component

3. Event Naming

Use descriptive, lowercase event names:

// ✅ GOOD
'itemselected', 'valuechanged', 'datarefresh'

// ❌ BAD
'click', 'change', 'update' // Too generic

4. Data Flow Direction

Prefer one-way data flow:

  • Data flows down (parent → child via properties)
  • Events flow up (child → parent via custom events)

5. Avoid Direct DOM Manipulation

Use LWC’s reactive properties instead of directly manipulating child component DOM:

// ❌ BAD: Direct DOM access
const child = this.template.querySelector('c-child');
child.shadowRoot.querySelector('.button').click();

// ✅ GOOD: Use component methods
const child = this.template.querySelector('c-child');
child.performAction();

6. Memory Management

Always clean up subscriptions and event listeners:

disconnectedCallback() {
    // Unsubscribe from message channels
    if (this.subscription) {
        unsubscribe(this.subscription);
    }
    
    // Remove event listeners if added manually
    // (Most are handled automatically by LWC)
}

Common Patterns Summary

Pattern Use Case Implementation
Parent → Child Pass data to child @api properties
Parent → Child Call child method Template reference + @api method
Child → Parent Notify parent of action Custom events
Sibling → Sibling Share data between siblings Events through common parent
Unrelated Components Cross-component communication Lightning Message Service
Component → Apex Get data reactively @wire service
Component → Apex Call Apex on demand Imperative Apex calls

Summary

Effective component communication is essential for building maintainable LWC applications:

  1. Use @api properties for parent-to-child data flow
  2. Use custom events for child-to-parent communication
  3. Route sibling communication through common parent
  4. Use Lightning Message Service for unrelated components
  5. Use @wire for reactive Apex data, imperative calls for on-demand
  6. Follow naming conventions and best practices
  7. Clean up subscriptions to prevent memory leaks

By understanding these patterns and when to use each, you’ll build components that communicate effectively while maintaining clear separation of concerns and good performance.