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:
- Parent-to-Child: Using properties and method calls
- Child-to-Parent: Using events
- Sibling Components: Using events through a common parent
- Unrelated Components: Using Lightning Message Service (pub/sub)
- 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
@apito 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
@apito expose methods to parent - Access child using
querySelectoror 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
CustomEventfor child-to-parent communication - Pass data via
detailproperty - Set
bubbles: trueandcomposed: truefor 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
disconnectedCallbackto 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
@apiproperties 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
@wirefor 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:
- Use
@apiproperties for parent-to-child data flow - Use custom events for child-to-parent communication
- Route sibling communication through common parent
- Use Lightning Message Service for unrelated components
- Use
@wirefor reactive Apex data, imperative calls for on-demand - Follow naming conventions and best practices
- 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.