Skip to main content

๐Ÿ—ฃ️ Component Communication in LWC: @api, Events & LMS Explained

๐Ÿ’ฌ In plain words: In Lightning Web Components (LWC), there are three distances your data can travel, and three exact tools to use. To talk down (Parent → Child), use @api properties. To talk up (Child → Parent), fire a CustomEvent. To talk to strangers (components anywhere else on the page), use the Lightning Message Service (LMS), which acts like a page-wide radio channel.
๐Ÿ“Œ Example: E-Commerce Store

A user clicks "Add to Cart" on a product tile (Child). The tile fires a CustomEvent('addtocart') upwards. The main product grid (Parent) catches that event, updates its internal state, and passes the new cart count down to the cart badge (another Child) via an @api property. Meanwhile, a mini-cart widget living totally separately in the page header listens to a Lightning Message Service (LMS) channel to know the cart was updated.

๐Ÿ“ก The 4 Directions of Communication

To keep components decoupled and reusable, you must strictly follow these unidirectional data flow patterns:

  • Parent to Child (Down): The parent passes data down by setting public @api properties on the child, or by calling a public @api method on the child element.
  • Child to Parent (Up): The child dispatches a standard JavaScript CustomEvent. The parent listens for it using an on- handler in its HTML template.
  • Sibling to Sibling (Across): Siblings never talk directly! Child A fires an event up to the shared Parent. The Parent catches it, updates its state, and passes the new data down to Child B via @api. The Parent acts as the traffic junction.
  • Unrelated Components (Anywhere): For components that do not share a parent (different page regions, utility bars, or even crossing between LWC, Aura, and Visualforce), use the Lightning Message Service (LMS).
๐Ÿง  The Golden Rule: Data flows DOWN via properties (@api). Actions flow UP via events (CustomEvent). Strangers talk over the radio (LMS).

๐Ÿงญ 360 Card — Component Communication

๐ŸŽฏ Rule: Strictly enforce the three distances. Never use LMS when a simple parent-child event will suffice.

๐Ÿš€ Gain: You avoid "spaghetti state." By enforcing unidirectional data flow, you always know exactly where a piece of data lives and how it changes.

๐Ÿ’ฐ Price: The shared parent becomes the single source of truth (and the traffic junction) for sibling communication, which can bloat the parent component if not managed cleanly.

๐Ÿ›‘ Limits: CustomEvent properties bubbles and composed default to false. A composed event pierces the shadow boundary and can be heard anywhere—it is powerful but leaky.

๐Ÿ”ฎ The Future: The choices you make here dictate whether legacy Aura or Visualforce components can participate. LMS is the only bridge that spans all three frameworks simultaneously.

๐Ÿ™‹ Core Q&A & Scenarios

Q: Two child LWCs under one parent must communicate—Child A's selection needs to update Child B. How do you wire it?
๐ŸŽฏ Say this first: Child A fires a CustomEvent upward. The parent catches it, updates its state, and passes the state down to Child B via @api. The parent acts as the traffic junction.

Detailed breakdown:

  • Child A dispatches the event: this.dispatchEvent(new CustomEvent('selectionchange', { detail: { id } })). It does not need bubbling, because the parent listens directly on the child's tag.
  • The parent handles the event, stores selectedId in its own state, and passes it down to Child B via an @api property.
  • Child B reacts via a setter or a getter on that property.
  • ๐Ÿšจ Red Flag: Reaching for LMS to communicate between two direct siblings is a classic sign of over-engineering. LMS is strictly for components that do not share a convenient ancestor.

๐Ÿ’ป Code Example: Sibling Communication

// 1. childA.js fires the event upward
this.dispatchEvent(new CustomEvent('selectionchange', { detail: { id: this.value } }));

<!-- 2. parent.html listens and passes data down -->
<c-child-a onselectionchange={handleSelection}></c-child-a>
<c-child-b selected-id={selectedId}></c-child-b>

// 3. parent.js reads the payload and updates state
handleSelection(evt) { 
  this.selectedId = evt.detail.id; 
}
  
Q: Explain bubbles and composed on a CustomEvent. When do you need them and what is the risk?

A: By default, both are false, meaning the event is only heard by the element directly wrapping the dispatcher. This is the safest, recommended contract.

  • bubbles: true lets the event travel up the DOM tree, but it stops at the shadow boundary.
  • composed: true lets the event pierce through the shadow boundary and escape into the enclosing DOM.
  • The Risk: A bubbling, composed event leaks into DOM you do not own. Unintended listeners might catch it, creating invisible, impossible-to-debug coupling. Use it as an absolute last resort.
Q: A utility-bar component needs to update components on a record page, including a legacy Aura component. How do you design this channel?

A: You must use the Lightning Message Service (LMS). Define a message channel in your metadata. The utility bar publishes the event. The subscribers on the record page must subscribe using APPLICATION_SCOPE. (Because the utility bar lives outside the active page region, the default active-area scope would miss it). Always remember to unsubscribe in the disconnectedCallback! LMS is the only supported bridge across LWC, Aura, and Visualforce.

Q: The screen must reflect a change that happens seconds later in a backend system. How do you design the UX?
๐ŸŽฏ Say this first: Never make the user refresh the page. Push the change to the screen using Platform Events, and be honest about the pending state while they wait.

A: The backend publishes a Platform Event (or CDC). The LWC subscribes using the empApi module. While waiting for the event to arrive, show a truthful pending state (e.g., "Payment Processing"). For slow synchronous Apex callouts, use the Continuation pattern to park the request and free up the worker thread, showing a spinner until the response arrives.