Event Bubbling
Event bubbling allows child components to send notifications up through the component hierarchy, reaching ancestors multiple levels above automatically.
The Basics
By default, notifyParent() only sends events to the immediate parent. Enable bubbling with the third parameter to propagate events up the entire hierarchy:
// Notify immediate parent only (default)
this.notifyParent('eventType', data);
// Bubble through all ancestors
this.notifyParent('eventType', data, true);
Bubbling in Action
When bubbling is enabled, events travel up through each ancestor's receiveFromChild() method until stopped or the root is reached:
<div id="root-app">
<h2>Root - Errors: {{errorCount}}</h2>
<div id="parent-app">
<h3>Parent - Status: {{status}}</h3>
<div id="child-app">
<h4>Child</h4>
<button data-pac-bind="click: triggerError">Trigger Error</button>
</div>
</div>
</div>
<script>
const child = wakaPAC('#child-app', {
triggerError() {
// Event bubbles to parent, then grandparent
this.notifyParent('error', {
message: 'Validation failed',
field: 'email'
}, true);
}
});
const parent = wakaPAC('#parent-app', {
status: 'OK',
receiveFromChild(eventType, data, childComponent) {
if (eventType === 'error') {
this.status = 'Child error: ' + data.field;
console.log('Parent received error');
// Event continues to grandparent automatically
}
}
});
const root = wakaPAC('#root-app', {
errorCount: 0,
receiveFromChild(eventType, data, childComponent) {
if (eventType === 'error') {
this.errorCount++;
console.log('Root caught error:', data.message);
}
}
});
</script>
Controlling Propagation
Any ancestor can stop the event from bubbling further by returning false from receiveFromChild():
<div id="root-app">
<h2>Root</h2>
<p>{{rootMessage}}</p>
<div id="middle-app">
<h3>Middle</h3>
<p>{{middleMessage}}</p>
<div id="child-app">
<h4>Child</h4>
<button data-pac-bind="click: sendEvent">Send Event</button>
</div>
</div>
</div>
<script>
const child = wakaPAC('#child-app', {
sendEvent() {
this.notifyParent('notification', {
text: 'Hello from child'
}, true);
}
});
const middle = wakaPAC('#middle-app', {
middleMessage: '',
receiveFromChild(eventType, data, childComponent) {
if (eventType === 'notification') {
this.middleMessage = 'Received: ' + data.text;
// Stop propagation - root never receives this event
return false;
}
}
});
const root = wakaPAC('#root-app', {
rootMessage: 'Waiting...',
receiveFromChild(eventType, data, childComponent) {
// Never called because middle stopped propagation
this.rootMessage = 'Should not see this';
}
});
</script>
Propagation Control
| Return Value | Behavior |
|---|---|
false |
Stop bubbling immediately - event is handled |
true |
Continue bubbling to next ancestor |
undefined (no return) |
Continue bubbling (default behavior) |
When to Use Bubbling
| Scenario | Recommendation |
|---|---|
| Direct parent-child communication | No - Default behavior is simpler |
| Global error handling at root | Yes - Centralized error logging/handling |
| Authentication/logout events | Yes - App-wide state changes |
| Form field validation | No - Parent form handles directly |
| Theme/language changes | Yes - Root manages global settings |
| Component-specific actions | No - Keep communication local |
Best Practices
- Default to non-bubbling: Only enable bubbling for events that genuinely need to reach distant ancestors
- Stop when handled: Return
falseonce the event is fully processed to prevent unnecessary propagation - Use consistent event names: Establish naming conventions for bubble events (e.g., "error", "auth:logout", "theme:change")
- Document bubble behavior: Clearly indicate which events bubble and why in your component documentation
- Consider alternatives: For truly global state, a dedicated state manager or direct root access might be clearer than relying on bubbling
- Avoid overuse: Too many bubbling events make the event flow hard to trace and debug
Common Patterns
Centralized Error Handler
// Root component catches all errors
const root = wakaPAC('#app', {
errors: [],
receiveFromChild(eventType, data, childComponent) {
if (eventType === 'error') {
this.errors.push({
timestamp: new Date(),
source: childComponent.container.id,
...data
});
// Stop bubbling - errors handled at root
return false;
}
}
});
// Any descendant can report errors
this.notifyParent('error', { message: 'Failed to save' }, true);
Authentication State
// Deep nested component triggers logout
this.notifyParent('auth:logout', { reason: 'session_expired' }, true);
// Root clears user state
receiveFromChild(eventType, data) {
if (eventType === 'auth:logout') {
this.currentUser = null;
this.redirectToLogin();
return false; // Handled at root
}
}
Performance Note: Bubbling adds minimal overhead - each ancestor's
receiveFromChild() is called synchronously in sequence. Stopping propagation early with return false prevents unnecessary ancestor processing.