LWC Zero to Hero Module 6 — Parent-Child Communication with Custom Events
📡 LWC Zero to Hero — Module 6 of 15
Parent-Child Communication
Module 5 taught data flowing DOWN via @api. Now data flows UP — from child to parent — using custom events. This completes the full two-way communication picture.
Module 6 of 15 (counting Module 0 as a bonus prerequisite) · Phase 3: Component Communication (Begins!)
🚀 Welcome to Phase 3. Modules 2-5 taught you how a SINGLE component works internally. Modules 6-8 teach how MULTIPLE components talk to each other — the skill that turns isolated building blocks into a real application.
🎯 What You'll Master in This Module
A parent can hand data DOWN to a child easily (Module 5's @api properties). But how does a CHILD tell its parent "something happened — here's the data"? A child cannot directly call a method or set a property on its parent (that would break component isolation). Instead, LWC uses the browser's native CustomEvent system — the child "fires" an event, and the parent "listens" for it.
✓ The CustomEvent constructor — creating an event with a name
✓ Passing data UP through the event's detail property
✓ Listening for custom events in the parent's template
✓ Event bubbling — how far an event travels by default
✓ The composed option — crossing shadow DOM boundaries
✓ When and where to call dispatchEvent() correctly
✓ Anti-patterns — when events are the wrong tool for the job
📋 In This Module
Concept 1 of 7
The Two-Way Communication Picture
LWC components communicate in exactly two directions, using two completely different mechanisms. DOWN (parent → child) uses
@api properties (Module 5) — the parent sets values directly. UP (child → parent) uses custom EVENTS — the child fires a signal, and the parent decides whether/how to react. There is no third mechanism; every parent-child interaction is one of these two.⚡ Why This Matters
A child component can NEVER directly modify its parent's data or call the parent's methods — this isolation is intentional and important for component reusability. Events are the only sanctioned way for a child to communicate upward, and understanding WHY this restriction exists makes the event system feel like a deliberate design choice rather than unnecessary complexity.
Parent
↓ @api property
Child
Child
custom event ↑
Parent
🛠️ Practical Mini-Implementation
A concrete example you'll build in this module: a
c-rating-stars child component (from Module 5's exercise) receives its initial value via @api value (DOWN), and whenever the user clicks a new star rating, it fires a custom event carrying the new rating value (UP) so the parent can save it. Both directions working together is what makes the component genuinely interactive, not just a one-way display.⚠️ Common Gotcha
Beginners sometimes try to have a child directly call a method ON the parent, or directly read/write a parent's property — neither is possible in LWC's architecture, and there's no decorator or trick to bypass this. If you find yourself wanting a child to "reach up" into its parent, that's the signal you need a custom event instead.
Concept 2 of 7
The CustomEvent Constructor
CustomEvent is a standard Web API (not LWC-specific — it's part of the browser platform itself) used to create a named event object. The child component creates one of these, then "fires" it using this.dispatchEvent(), which is a method every component inherits from LightningElement (recall Module 2's discussion of what extending LightningElement gives you).⚡ Why This Matters
Because this is a genuine Web Standards API (not a Salesforce invention), the knowledge transfers directly to any other web framework — understanding CustomEvent here is broadly valuable, not just an LWC-specific trick.
CREATING AND DISPATCHING THE SIMPLEST POSSIBLE CUSTOM EVENT
export default class RatingStars extends LightningElement {
handleStarClick(event) {
this.dispatchEvent(new CustomEvent('ratingchange'));
}
}
// 'ratingchange' is the event NAME — entirely your choice, with rules (Concept naming)
// This fires the event, but carries no actual data yet — Concept 3 fixes that
EVENT NAMING RULES
// ✅ Valid event names — lowercase, no special characters
new CustomEvent('ratingchange');
new CustomEvent('orderselected');
new CustomEvent('save');
// ❌ Invalid — LWC enforces lowercase, alphanumeric only (no camelCase, no dashes, no underscores)
new CustomEvent('ratingChange'); // camelCase not allowed
new CustomEvent('rating-change'); // dashes not allowed
new CustomEvent('rating_change'); // underscores not allowed
🛠️ Practical Mini-Implementation
Good event names describe WHAT HAPPENED, often ending in a verb-like suffix:
orderselected, ratingchange, recordsaved, filterapplied. Avoid vague names like update or change alone if your component might fire multiple different kinds of events — be specific enough that a parent reading your component's documentation immediately understands what triggered it.⚠️ Common Gotcha
The strict lowercase-alphanumeric-only naming rule is enforced by LWC specifically (stricter than raw browser CustomEvent, which technically allows more characters) — if you write camelCase or dashed event names, LWC will throw a console error at runtime. This trips up developers coming from React (where custom prop/callback names are typically camelCase) expecting similar naming freedom.
Concept 3 of 7
Passing Data with detail
An event with no data is rarely useful on its own. The
CustomEvent constructor accepts a second argument — an options object — where the detail property carries whatever payload you want to send up to the parent. This is THE mechanism for sending actual data through a custom event.⚡ Why This Matters
Almost every real custom event needs to carry information — which item was clicked, what the new value is, which record was selected.
detail is how that information actually travels from child to parent.DISPATCHING AN EVENT WITH A detail PAYLOAD
export default class RatingStars extends LightningElement {
@api value = 0;
handleStarClick(event) {
const newRating = parseInt(event.target.dataset.rating, 10);
this.dispatchEvent(
new CustomEvent('ratingchange', {
detail: { rating: newRating, previousRating: this.value }
})
);
}
}
// detail can be ANY JavaScript value — a primitive, an object, even an array
// Here we send an object with two pieces of information at once
🛠️ Practical Mini-Implementation
Sending an OBJECT in
detail (rather than a single primitive value) is a good default habit, even when you only need one piece of data right now — it makes your event easy to extend later (adding a second field to the object) without changing the event's fundamental shape or breaking existing parent handlers that only read the one field they care about.⚠️ Common Gotcha
The parent accesses this payload as
event.detail — NOT event.target.detail or any other variation. This is a precise, fixed API: the data you put in the detail property of the constructor's options object always comes back out as event.detail in the parent's handler, covered fully in Concept 4 next.Concept 4 of 7
Listening for Custom Events in the Parent
The parent listens for a child's custom event by adding an
on<eventname> attribute directly on the child's tag in the template — exactly the same pattern you've already used for standard DOM events like onclick. The handler function receives the event object, from which you read event.detail.⚡ Why This Matters
This
onclick-style syntax means custom events feel completely natural once you know standard event handling — there's no special new syntax to learn for "listening," only for how the child creates and sends the event.PARENT LISTENS using on<eventname> syntax
<!-- parentComponent.html -->
<template>
<c-rating-stars
value={currentRating}
onratingchange={handleRatingChange}>
</c-rating-stars>
</template>
// parentComponent.js
export default class ParentComponent extends LightningElement {
currentRating = 3;
handleRatingChange(event) {
console.log(event.detail.rating); // the new rating
console.log(event.detail.previousRating); // the old rating
this.currentRating = event.detail.rating;
}
}
// Notice: event name "ratingchange" becomes attribute "onratingchange" — always lowercase, "on" + name
🛠️ Practical Mini-Implementation
This is exactly the two-way binding pattern you'll use constantly: the parent passes
currentRating DOWN as the value property, and listens for ratingchange events to update currentRating whenever the child reports a change — completing a controlled, predictable data flow loop entirely owned by the parent.⚠️ Common Gotcha
The attribute is ALWAYS
on + the exact event name, all lowercase, with no separators — an event named ratingchange is listened to with onratingchange, never on-rating-change or onRatingChange. This mirrors the strict lowercase event-naming rule from Concept 2.Concept 5 of 7
Event Bubbling & the composed Option
By default, a custom event in LWC does NOT bubble — it's only visible to the IMMEDIATE parent listening directly on the child's tag. Two additional options control how far an event travels:
bubbles (does it propagate up through ancestor elements, not just the direct parent?) and composed (can it cross the Shadow DOM boundary, recall Module 2/3's mention of Shadow DOM, to be heard by components further up the tree)?⚡ Why This Matters
A grandparent component (two levels up, not the direct parent) will NEVER hear a default custom event — understanding why, and how to deliberately enable wider propagation, is essential for components nested more than one level deep.
DEFAULT — no bubbles, no composed (most common, most contained)
this.dispatchEvent(new CustomEvent('ratingchange', {
detail: { rating: newRating }
// bubbles: false (default) — only the DIRECT parent can hear this
// composed: false (default) — does not cross shadow DOM boundary
}));
EXPLICITLY ENABLING bubbles and composed
this.dispatchEvent(new CustomEvent('ratingchange', {
detail: { rating: newRating },
bubbles: true, // now travels up through ALL ancestor elements, not just direct parent
composed: true // now crosses shadow DOM boundaries too
}));
// Even with bubbles+composed, a GRANDPARENT must still explicitly listen
// for the event somewhere along that bubble path — it's not automatic "magic"
🛠️ Practical Mini-Implementation
For straightforward direct parent-child communication (the vast majority of cases), leave both options at their default
false — this keeps your component's communication contained and predictable. Only reach for bubbles: true, composed: true when you specifically need an event to travel through multiple component levels, and even then, consider whether Lightning Message Service (Module 7) is a cleaner solution for cross-hierarchy communication.⚠️ Common Gotcha
Setting
bubbles: true WITHOUT also setting composed: true often doesn't achieve what developers expect — without composed: true, the event still can't escape the component's own shadow DOM boundary to reach ancestors further up. For events intended to travel beyond the direct parent, you almost always need BOTH options set to true together, not just one.Concept 6 of 7
dispatchEvent Timing & Common Patterns
WHERE in your code you call
dispatchEvent() matters — it should happen AFTER you've finished whatever local state update triggered the need to notify the parent, ensuring the child's own internal state is already consistent before the parent reacts to the notification.⚡ Why This Matters
Dispatching too early (before updating your own state) can create timing bugs where the parent's reaction happens based on stale information, or where the child's own template renders an inconsistent intermediate state briefly.
PATTERN — update local state FIRST, then notify
handleStarClick(event) {
const newRating = parseInt(event.target.dataset.rating, 10);
const previousRating = this.value;
// (if rating-stars manages its OWN local copy, update it first)
// then notify the parent with both values:
this.dispatchEvent(
new CustomEvent('ratingchange', {
detail: { rating: newRating, previousRating }
})
);
}
DEBOUNCING — avoiding event spam on rapid-fire input
export default class SearchBox extends LightningElement {
searchTimeout;
handleKeyUp(event) {
const searchValue = event.target.value;
clearTimeout(this.searchTimeout); // cancel any pending dispatch
this.searchTimeout = setTimeout(() => {
this.dispatchEvent(
new CustomEvent('search', { detail: { searchTerm: searchValue } })
);
}, 300); // only fires 300ms after the user stops typing
}
disconnectedCallback() {
clearTimeout(this.searchTimeout); // recall Module 2's cleanup gotcha!
}
}
🛠️ Practical Mini-Implementation
The debounce pattern above is one of the most practically valuable patterns in this whole module — without it, a search box firing an event on every single keystroke could trigger an Apex call (Module 9-10) for every character typed, which is wasteful and slow. Debouncing fires the event only after the user pauses, dramatically reducing unnecessary work.
⚠️ Common Gotcha
Notice the debounce example also clears the timeout in
disconnectedCallback() — this directly applies Module 2's cleanup lesson. Forgetting this means a pending debounced event could still try to fire after the component has already been removed from the page, which is wasted work at best and a source of confusing bugs at worst.Concept 7 of 7
Communication Anti-Patterns
Custom events are powerful, but overusing them — or using them for the wrong job — creates components that are harder to understand and maintain. Let's cover the most common mistakes.
⚡ Why This Matters
Senior-level code reviews frequently flag these exact anti-patterns — recognizing them in your OWN code before submitting it for review (or before an interviewer asks you to spot the problem) is a mark of genuine experience, not just textbook knowledge.
❌ ANTI-PATTERN 1: Events for data that should just be an @api property
// Overkill — firing an event just to report a STATIC value that never changes
connectedCallback() {
this.dispatchEvent(
new CustomEvent('componentready', { detail: { componentName: 'rating-stars' } })
);
}
// If the parent always knows this value anyway, this event adds complexity for no benefit
❌ ANTI-PATTERN 2: "Prop drilling" — passing data through many uninvolved layers
// Grandchild needs to tell Grandparent something, forced through an uninvolved Parent:
// Grandchild fires event → Parent listens ONLY to re-fire the same event upward →
// Grandparent finally listens. The middle "Parent" does nothing but relay.
// This is a sign you may want Lightning Message Service instead (Module 7)
✅ BETTER: reserve events for genuine USER ACTIONS or MEANINGFUL state changes
// Good use cases for custom events:
new CustomEvent('ratingchange', {...}); // user took an action
new CustomEvent('recordsaved', {...}); // meaningful state change completed
new CustomEvent('validationerror', {...}); // something the parent genuinely needs to react to
🛠️ Practical Mini-Implementation
A good litmus test before adding a new custom event: "does the PARENT need to make a decision or take an action based on this?" If the answer is genuinely yes, an event is the right tool. If you're just reporting information for its own sake with no expected parent reaction, reconsider whether you need the event at all.
⚠️ Common Gotcha — Looking Ahead
The "prop drilling" anti-pattern (Anti-Pattern 2) is specifically what Module 7's Lightning Message Service and Pub-Sub pattern solve — when components that AREN'T in a direct parent-child relationship need to communicate, forcing events through uninvolved intermediate components is exactly the pain point that makes the next module's tools necessary.
💬 Module 6 Interview Questions (6)
Q1Why can't a child component directly call a method on its parent or modify the parent's properties, and what mechanism does LWC provide instead?
LWC deliberately isolates components from each other to keep them reusable and predictable — a child component has no direct reference to its parent and cannot call its methods or modify its properties, since that would create tight coupling and make components fragile when reused in different contexts. Instead, LWC provides custom events as the sanctioned mechanism for child-to-parent communication: the child creates and dispatches a CustomEvent, and the parent, which does have a reference to the child through its template, can choose to listen for that event and react accordingly, keeping the parent in control of how it responds.
"Components are intentionally isolated for reusability, so a child cannot directly call or modify its parent; instead, the child dispatches a CustomEvent which the parent can optionally listen for, keeping the parent in control of the reaction."
Q2What naming restrictions does LWC enforce on custom event names, and why might a developer coming from React be surprised by this?
LWC requires custom event names to be entirely lowercase and alphanumeric, with no camelCase, dashes, or underscores permitted — names like ratingChange, rating-change, or rating_change will all cause a runtime console error. This is stricter than the raw browser CustomEvent API, which technically allows more flexible naming. Developers coming from React may be surprised because React's convention for custom callback props is typically camelCase (like onRatingChange passed as a prop), which is a completely different naming pattern than LWC's strict lowercase-only event name requirement.
"LWC enforces strictly lowercase, alphanumeric-only event names with no camelCase, dashes, or underscores, which is stricter than the underlying browser API and differs from React's camelCase callback prop conventions, often surprising developers transitioning between frameworks."
Q3How does a parent component access the data sent by a child's custom event, and where does that data originate in the child's code?
The parent accesses the data through the event.detail property within its event handler function. This data originates from the detail property that the child specified in the second argument (the options object) passed to the CustomEvent constructor when creating the event — for example, new CustomEvent('ratingchange', { detail: { rating: 5 } }). Whatever value or object structure the child places into that detail property during event creation is exactly what arrives as event.detail in the parent's corresponding handler, with no transformation or renaming along the way.
"The parent reads custom event data via event.detail in its handler, which directly corresponds to the detail property the child set in the CustomEvent constructor's options object when creating and dispatching the event."
Q4By default, can a grandparent component (two levels up) listen for and receive a custom event dispatched by a grandchild? What needs to change for this to work, and is that alone sufficient?
By default, no — a custom event in LWC does not bubble or cross shadow DOM boundaries unless explicitly configured to do so, meaning only the immediate, direct parent component listening on the child's tag can receive the event by default. To allow the event to travel further up the component tree, the dispatching component must set both bubbles: true and composed: true in the CustomEvent options object, since bubbles alone allows propagation through ancestor elements but composed is additionally required to cross shadow DOM boundaries between components. However, even with both options enabled, this alone is not sufficient — some component along that bubble path, such as the grandparent itself or an intermediate component, must still explicitly add an event listener for that specific event name in order to actually receive and react to it.
"By default, only the direct parent can hear a custom event; reaching a grandparent requires setting both bubbles:true and composed:true together, and even then some component along the path must still explicitly listen for the event — propagation alone doesn't guarantee anything is listening."
Q5Why would you implement debouncing before dispatching a custom event from a search input, and how would you ensure no debounced event fires after the component is removed from the page?
Debouncing delays dispatching the event until the user has paused typing for a set period, rather than firing the event on every single keystroke — without this, a search box could trigger expensive operations like Apex calls on every character typed, which wastes resources and degrades performance, especially for fast typists. This is typically implemented using setTimeout combined with clearTimeout, where each new keystroke cancels the previous pending timeout and starts a fresh one, so the event only actually dispatches once typing has stopped for the specified delay. To prevent a debounced event from firing after the component has been removed from the page, the pending timeout should be explicitly cleared inside disconnectedCallback(), directly applying the lifecycle cleanup principle introduced in Module 2 to avoid wasted work or unexpected behavior from a component that no longer exists.
"Debouncing with setTimeout/clearTimeout prevents firing an event on every keystroke, reducing unnecessary expensive operations; the pending timeout should always be cleared in disconnectedCallback() to prevent a debounced event from firing after the component has already been removed."
Q6What is the "prop drilling" anti-pattern in the context of LWC component communication, and what is generally a better solution for it?
Prop drilling occurs when a deeply nested component (a grandchild or further descendant) needs to communicate with a distant ancestor, but the only available mechanism — direct parent-child custom events — forces every intermediate component along that hierarchy to listen for and simply re-dispatch the same event upward, purely to relay it, even though those intermediate components have no genuine involvement in that communication. This adds unnecessary complexity and coupling to components that should otherwise be unrelated to that specific interaction. A generally better solution, covered in the next module, is Lightning Message Service, which allows components anywhere in the application — regardless of their position in the component hierarchy, and even across Aura or Visualforce boundaries — to communicate directly without requiring every intermediate layer to participate in relaying the message.
"Prop drilling forces uninvolved intermediate components to relay events purely to pass data between distant components in a hierarchy; Lightning Message Service solves this by allowing components anywhere in the app to communicate directly, without requiring intermediate layers to participate."
📝 Module 6 Recap — Parent-Child Communication Mastered
✅ Two directions only: @api properties go DOWN (Module 5), custom events go UP — no other mechanism exists
✅ CustomEvent names must be strictly lowercase and alphanumeric — no camelCase, dashes, or underscores
✅ Data travels via the detail property in the CustomEvent constructor's options object
✅ Parents listen using on<eventname> syntax directly on the child's tag, identical to standard DOM event binding
✅ Events don't bubble or cross shadow DOM by default — both bubbles:true AND composed:true are needed together for wider propagation
✅ Debounce rapid-fire events (like search input) with setTimeout/clearTimeout, always cleaned up in disconnectedCallback()
✅ Avoid prop drilling — forcing uninvolved components to relay events — and avoid events for static data that fits better as an @api property
🎯 Before Moving to Module 7...
Try this: finish the c-rating-stars exercise from Module 5 by adding the event side — clicking a star should fire a ratingchange custom event carrying the new value in detail, which the parent listens for and uses to update its own tracked rating. Then deliberately try making a "grandchild" version (parent containing a middle component containing rating-stars) and notice the grandparent can't hear the event without prop drilling through the middle component. Module 7 solves exactly that problem with Lightning Message Service and the pub-sub pattern.
← Module 5: @api Properties & Public Methods
Module 7: Pub-Sub & Lightning Message Service → (Coming Soon)
☕
☕ Enjoyed this article?
SF Interview Pro is 100% free and maintained by a Salesforce professional. No ads, no paywalls, and no signup required. If this guide helped you prepare for an interview, earn a certification, or grow your Salesforce career, consider buying me a coffee! ☕💜
rajnishrk264@ybl
Scan with GPay, PhonePe, Paytm, or BHIM
📚 Keep Preparing
New interview questions every week 🚀
Follow for fresh Salesforce Q&A, free courses, and real interview experiences — straight from the trenches.
👥 Follow on LinkedIn