Component Communication
Break an interface into components and you immediately create a new question: how do they talk to each other? A toolbar button needs to affect a list, a form has to update a summary, a notification reacts to a cart change. Answer this casually, with each component reaching directly into whichever other ones it needs, and you get a web of tangled references that breaks in unexpected places. Component communication is about choosing channels that keep the parts coordinated without making them dependent on each other's internals.
This guide maps those channels to the relationships between components. It builds on state management and the events model from Foundations.
What you'll learn
The communication problem
Components are valuable precisely because they are self-contained, but an app needs them to act in concert. The tension is that every connection you add between two components is also a dependency, and dependencies are what make change risky.
Coupling is the cost to watch
When a component reaches directly into another to read or change its internals, the two are now coupled: you cannot move, reuse, or rework one without minding the other. The goal of every pattern below is to let components coordinate while staying loosely coupled, communicating through clear, intentional channels rather than by poking at each other. Keep coupling low and the app stays flexible; let it grow and every feature gets harder to add.
Parent and child: props down, events up
The most common relationship is also the one with the clearest answer. When one component renders another, data flows down and notifications flow up.
Data down, events up
// Parent passes data down; child reports back via a callback
ChildView({
value: state.value,
onChange: function (next) { update(next); }
});
The parent passes data into the child as props or inputs, and the child communicates back by emitting an event or calling a callback, never by editing the parent directly. This keeps the direction predictable, the same data-down idea behind unidirectional flow, and means the child knows nothing about its parent beyond the contract it was handed. Most communication in a well-structured app is just this pattern, repeated.
Lifting state up for siblings
Two components side by side cannot use props and events to reach each other, because neither renders the other. The standard fix is to give them a common point above.
A shared ancestor for siblings
When siblings need to share a value, lift that state up to their nearest common ancestor, which then passes it down to both and handles the events coming back up. A change in one sibling updates the shared state, and the other sibling re-renders from it, without the two ever referencing each other. Lifting state keeps the single-source-of-truth principle intact while letting neighbours stay in step, and it keeps the shared value close to where it is actually used.
Shared state and the store
Lifting works until the common ancestor is so far up the tree that the data has to pass through many components that do not care about it. That symptom has a name, and a better answer.
Avoiding prop drilling with a store
Threading a prop through layers of intermediaries just to reach a deep component is prop drilling, and it couples all those middle components to data they never use. For state shared across distant parts of the app, put it in a shared store or context that the consumers read directly, as the state-management guide describes. The store becomes the channel, so far-apart components coordinate through shared state instead of a fragile chain of hand-offs.
Pub-sub and the event bus
Sometimes components need to react to something without any structural relationship at all, and without either side knowing the other exists. That is what a publish-subscribe channel provides.
Decoupled messaging with events
// Decoupled: one part publishes, others subscribe
bus.emit('cart:updated', cart);
bus.on('cart:updated', function (cart) {
// any interested component reacts
});
With an event bus, one component publishes a named event and any others subscribe, with no direct references between them, the broadcast form of the events pattern. It shines for cross-cutting concerns like a global notification or an analytics hook. The caution is that this decoupling can become too much of a good thing: when everything talks through an invisible bus, it gets hard to see who reacts to what, so reserve it for genuinely cross-cutting messages rather than routine parent-child talk.
Choosing the right channel
There is no single best channel; there is a best channel for each relationship. Picking by relationship, rather than by habit, is what keeps the communication map legible.
Match the pattern to the relationship
For parent and child, use props down and events up. For siblings, lift state to a shared ancestor. For distant or widely shared state, use a store. For truly cross-cutting, reference-free messages, use pub-sub, sparingly. Reaching for a global bus to connect a parent and its child, or drilling props to avoid a store, are both signs of a mismatch. Match the channel to the relationship and components stay decoupled while the whole app stays coordinated. The next guide goes deeper on the event-driven style that underlies several of these channels.
Frequently Asked Questions
How do components communicate in a frontend app?
A parent passes data to a child through props or inputs, and the child reports back by emitting events. Siblings communicate by lifting shared state to a common ancestor, and distant components talk through a shared store or an event bus rather than a long chain of intermediaries.
What is prop drilling and how do I avoid it?
Prop drilling is passing a prop through many intermediate components that do not use it, just to reach a deep one. You avoid it by putting the shared value in a store or context that the consuming component reads directly, so the in-between components no longer have to forward it.
What is an event bus or pub-sub in frontend?
An event bus is a decoupled messaging channel: a component publishes a named event, and any number of others subscribe to it, without holding references to each other. It suits cross-cutting concerns, but overusing it can hide who talks to whom and make behaviour hard to trace.
When should I lift state up versus use a global store?
Lift state to the nearest common ancestor when only a small subtree shares it, which keeps the state close to where it is used. Reach for a global store when the same state is needed across distant or many parts of the app, where lifting would push it awkwardly high.
Read next: back to the Frontend Architecture hub, or revisit state management for the store these channels share.
Want to see components and events in action?
The complete Backbone guide builds views that talk through events, not tangled references.
Explore the Backbone Guide →