Briefly: down the tree use public properties, up the tree use a CustomEvent, and sideways use the Lightning Message Service. Three mechanisms, and the mistake that causes most of the confusion is reaching for the third when the first two would have worked, because a message channel has no shape and nothing tells you who is listening.
Parent to child: @api
A property marked @api is public. The parent sets it in its template, and the child re-renders when it changes.
// child.js
export default class Child extends LightningElement {
@api recordId;
@api label = 'Save';
}
<!-- parent.html -->
<c-child record-id={currentId} label="Confirm"></c-child>
Note the case change: recordId in JavaScript, record-id in markup. HTML attribute names are case-insensitive, so the framework translates camelCase to kebab-case, and a property that quietly never arrives is usually this.
Two rules people break. Do not mutate an @api property inside the child — it belongs to the parent, and changing it produces behaviour that depends on render order. And if the property is an object or array, the child must not mutate its contents either, because the parent holds the same reference.
A parent can also call a child’s @api method directly through this.template.querySelector('c-child').focusFirstField(). That is legitimate for imperative things like focus or validation, and wrong for anything that is really state.
Child to parent: CustomEvent
The child announces that something happened. It does not know or care who is listening.
// child.js
handleSelect(event) {
this.dispatchEvent(new CustomEvent('rowselect', {
detail: { recordId: event.currentTarget.dataset.id }
}));
}
<!-- parent.html -->
<c-child onrowselect={handleRowSelect}></c-child>
The event name must be lowercase with no hyphens, or the on handler in the template will not bind. rowselect works, rowSelect and row-select do not, and this fails silently.
Keep detail to primitives. In Lightning Web Security an object passed through detail may be given to the listener as a copy, so a listener mutating it is a bug that appears in one context and not another. Send an ID and let the parent fetch what it needs.
Events do not bubble out of the shadow boundary by default, and that default is correct. Set bubbles: true, composed: true only when you have a reason you can articulate, because a composed event escapes the component entirely and can be caught by things you did not write.
Sideways: Lightning Message Service
Two components on a page that are not related, or an LWC and an Aura component, or a Visualforce page. That is what a message channel is for, and it is the only thing it is for.
import { publish, MessageContext } from 'lightning/messageService';
import ORDER_CHANNEL from '@salesforce/messageChannel/Order_Selected__c';
@wire(MessageContext) messageContext;
publish(this.messageContext, ORDER_CHANNEL, { recordId: this.selectedId });
The cost is that the channel is a broadcast with no contract. Nobody has to subscribe, and there is no error when nobody does. Debugging “the other component did not update” means checking three separate things: that the publish ran, that the subscriber exists on the page, and that the payload matched what the subscriber expected.
Always unsubscribe in disconnectedCallback. A subscription that outlives its component is a leak, and on a page where users move between records it accumulates.
The fourth option nobody mentions
Two components showing the same record do not need to talk at all. If both use the Lightning Data Service (getRecord, lightning-record-form, or updateRecord) the cache updates every consumer. One component saves, the other refreshes, and neither knows the other exists. Reach for a message channel only after checking that LDS does not already solve it, which is the same instinct that keeps components simple in stale data after a save.
Frequently asked
Can a child talk to a grandparent? Only by each layer re-dispatching, or by a composed event, or by a message channel. Re-dispatching is verbose and explicit. Pick it when the tree is shallow.
Why is my handler not firing? Almost always the event name has a capital or a hyphen, or the listener is on the wrong element.
Should I use a pub/sub library? No. That pattern predates the Lightning Message Service and the platform now supports the same thing properly.
Does any of this work across tabs? No. A message channel is scoped to one Lightning page in one browser tab.
