Skip to main content

Mastering Communication in Salesforce Lightning Web Components (LWC)

Updated Add a comment
In plain words: In Lightning Web Components (LWC), data flows down and events flow up. A parent passes data to a child through public @api properties, and can call a child's public @api methods. A child can't change the parent's data directly, so it sends a custom event with dispatchEvent, and the parent decides what to do with it.

Real Salesforce apps are built from many small components that work together. To keep them clean and reusable, each component needs clear rules for how it talks to the others. This post covers both directions, parent to child and child to parent, using one simple example: a parent shows a product, and a child lets the user pick a quantity and add it to a cart.

1. Understanding the Parent-Child Relationship

A parent component includes one or more child components in its HTML template. The parent owns the main data and passes it down. Communication follows two simple rules:

  • Downward (Parent to Child): The parent sets the child's public @api properties, or calls the child's public @api methods.
  • Upward (Child to Parent): The child tells the parent about user actions or changes by dispatching a CustomEvent.
360 Component Communication Card:
  • Pass Data Down: In the child, mark properties with @api. In the parent's HTML, set them with kebab-case attributes (camelCase itemName becomes item-name).
  • Call Child Methods: Mark a child method with @api, then call it from the parent through this.refs or this.template.querySelector().
  • Reactivity: When the parent changes a value it passes down, the child re-renders automatically.
  • Send Events Up: The child calls this.dispatchEvent(new CustomEvent('name', { detail })).
  • Listen in the Parent: Add onname={handler} on the child tag in the parent's HTML, and read the data from event.detail.
  • Unrelated Components: Use Lightning Message Service (LMS) when components don't have a parent-child relationship.

2. Parent to Child: Passing Data with @api

Step 1: Expose Public Properties in the Child
The child marks the properties it accepts with @api. It also keeps its own private state, like quantity, which only the child changes.
// childComponent.js
import { LightningElement, api } from 'lwc';

export default class ChildComponent extends LightningElement {
    // Public properties: set by the parent
    @api itemName = 'Default Item';
    @api itemPrice = 0;

    // Private state: owned and changed by the child
    quantity = 1;

    handleQuantityChange(event) {
        this.quantity = Number(event.target.value);
    }
}
<!-- childComponent.html -->
<template>
    <div class="slds-box slds-theme_shade">
        <p>Item Name: <strong>{itemName}</strong></p>
        <p>Price:
            <lightning-formatted-number value={itemPrice} format-style="currency" currency-code="USD">
            </lightning-formatted-number>
        </p>
        <lightning-input type="number" label="Quantity" min="1"
            value={quantity} onchange={handleQuantityChange}>
        </lightning-input>
        <lightning-button class="slds-m-top_small" label="Add to Cart"
            onclick={handleAddClick}>
        </lightning-button>
    </div>
</template>
Step 2: Pass Values from the Parent
The parent sets the child's public properties in its HTML. Remember: camelCase JavaScript names become kebab-case HTML attributes.
<!-- parentComponent.html -->
<template>
    <lightning-card title="Parent Container">
        <div class="slds-p-around_medium">
            <c-child-component
                lwc:ref="child"
                item-name={productName}
                item-price={productPrice}
                onadditem={handleAddItem}>
            </c-child-component>

            <p class="slds-m-top_medium">Last added: <strong>{lastAdded}</strong></p>
            <lightning-button class="slds-m-top_small" label="Reset Quantity"
                onclick={handleReset}>
            </lightning-button>
        </div>
    </lightning-card>
</template>
// parentComponent.js
import { LightningElement } from 'lwc';

export default class ParentComponent extends LightningElement {
    // All fields are reactive, so @track isn't needed here
    productName = 'Salesforce Enterprise License';
    productPrice = 150;
    lastAdded = 'Nothing yet';

    // handleAddItem and handleReset are shown in the next sections
}

3. Parent to Child: Calling Public Child Methods

Sometimes a parent needs the child to do something, like clear a form or refresh its data. Mark a child method with @api, then call it from the parent.

// childComponent.js (add this method)
@api
resetQuantity() {
    // Reset the child's OWN state, not its @api properties
    this.quantity = 1;
}
// parentComponent.js (add this method)
handleReset() {
    // lwc:ref="child" in the template gives a direct reference
    this.refs.child?.resetQuantity();

    // Older alternative:
    // this.template.querySelector('c-child-component')?.resetQuantity();
}

lwc:ref is the modern, simpler way to get a child element. this.refs is only available after the component renders, so call it from event handlers or renderedCallback(), not from the constructor.

4. Child to Parent: Sending Custom Events

Step 1: Dispatch an Event from the Child
When the user clicks "Add to Cart," the child packs the data into a custom event and sends it up.
// childComponent.js (add this method)
handleAddClick() {
    // Send simple, primitive values in detail
    this.dispatchEvent(new CustomEvent('additem', {
        detail: { itemName: this.itemName, quantity: this.quantity }
    }));
}

By default, the event doesn't bubble and stays inside the child's shadow tree. Only the component that contains the child (the parent) can hear it. That's what you want for most parent-child communication.

Step 2: Handle the Event in the Parent
The parent listens with on + the event name (onadditem, already in the parent template above) and reads the data from event.detail.
// parentComponent.js (add this method)
handleAddItem(event) {
    const { itemName, quantity } = event.detail;
    this.lastAdded = `${quantity} x ${itemName}`;
}

5. When Components Aren't Parent and Child

  • A Child Can't Call Parent Methods: The child has no reference to its parent. If the child needs the parent to act, it sends an event. That keeps the child reusable in any parent.
  • Lightning Message Service (LMS): When components aren't nested, for example two components in different regions of a Lightning record page, use LMS to publish and subscribe to messages across the page.

6. Common Traps & Best Practices

Developer Trap: Event Names with Capital Letters
Use only lowercase letters in event names, with no spaces or hyphens, and don't start them with "on." If you dispatch new CustomEvent('itemSelect'), a parent listening with onitemselect won't catch it, because HTML attributes are always lowercase and itemselect isn't the same event as itemSelect. Name it itemselect from the start. If you need to separate words, use an underscore.
Developer Trap: Changing @api Values in the Child
The parent owns the data it passes down. Objects and arrays passed through @api are read-only in the child, and changing them throws an error. Reassigning a passed-in value also goes against the data flow, and the parent will overwrite it on its next update. If the child needs a value changed, it should send an event so the parent can change it. If the child needs an editable copy, make a copy first.
Developer Trap: Setting bubbles and composed to true by Default
composed: true lets an event escape the shadow DOM and travel through the whole page, where any component can catch it. That makes the event part of your component's public API and can expose data you didn't mean to share. Leave both options off (the default) unless you really need the event to travel beyond the direct parent.
Core Rule: Data flows down through @api properties. Events flow up through CustomEvent. Never let a child change its parent's data directly.
  • Use Kebab-Case in HTML: camelCase properties like recordId are written in kebab-case in markup, like record-id.
  • Keep Payloads Small: Send IDs and other primitive values in event.detail rather than large objects. If you must send an object, send a copy so the receiver can't change the original.
  • Name Events by Action: Use clear names like recordchange or filterapply.
  • Validate Incoming Data: Use a getter and setter on an @api property when the child needs to check or transform a value before using it. Put @api on only one of the pair.
  • Boolean Defaults: A public Boolean property must default to false. That way, adding the attribute to the tag turns it on.
  • Plan First: Sketch your component tree and data flow before you build, so each component has one clear job.

Summary

Parent-child communication is a core LWC skill. Pass data down with @api properties, call child actions with @api methods and lwc:ref, and send changes back up with lowercase custom events. For components that aren't nested, use Lightning Message Service. Follow these rules and your components stay simple, reusable, and easy to maintain.

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.