In a large Backbone project, I experienced this issue.
Many people solve the problem with an event bus. To me, that makes debugging a huge pain and coupling rather difficult to analyze.
I solved it by passing a single flat context object that provides the API exposed by all of the ancestors of a component. Components can either pass this context unmodified to their children or make modified copies to add to the API seen by their children.
The advantage here is that intermediate layers of the tree don't need to know about APIs they don't directly consume, solving the problem of making every intermediary component mediate communication between parent and child.
As for the somewhat more difficult problem of allowing distant parts of the app to communicate (say, A and B), the key is abstracting objects for holding application state and communicating changes across the app into a new component C, which must be held by a common ancestor D of A and B. D then becomes responsible for mixing C's interface into the context object received by all its children. Typically D should be a controller-type component, mostly existing to serve as the nexus for a subsection of the app. D might also be the top level app object. But partitioning concerns into separate components like C avoids bloating D, and yields a lot of flexibility.
Other interesting patterns can be done as well. Optional hooks can be defined as context methods. The flat namespace of the context object also acts like a facade, decoupling the consumer of API methods from the actual provider. By making every member of the context a function (as opposed to data members), late coupling is achieved, which I found very useful in simplifying initialization dependencies.
On the whole, I found the flat context to be extremely flexible, and developed a number of patterns for solving particular problems. I have an unfinished blog post on the topic that I may one day get around to.
I'm using the exact same flow in my current project.
Shared data is stored in the state of a single component. Data propagates via props and since you've got a single source of truth you don't have to worry about data synchronization. To manipulate data, the component that owns the data exposes methods to children via context.
As a bonus, components become incredibly easy to test since both props and context are both explicitly passed to each component.
Many people solve the problem with an event bus. To me, that makes debugging a huge pain and coupling rather difficult to analyze.
I solved it by passing a single flat context object that provides the API exposed by all of the ancestors of a component. Components can either pass this context unmodified to their children or make modified copies to add to the API seen by their children.
The advantage here is that intermediate layers of the tree don't need to know about APIs they don't directly consume, solving the problem of making every intermediary component mediate communication between parent and child.
As for the somewhat more difficult problem of allowing distant parts of the app to communicate (say, A and B), the key is abstracting objects for holding application state and communicating changes across the app into a new component C, which must be held by a common ancestor D of A and B. D then becomes responsible for mixing C's interface into the context object received by all its children. Typically D should be a controller-type component, mostly existing to serve as the nexus for a subsection of the app. D might also be the top level app object. But partitioning concerns into separate components like C avoids bloating D, and yields a lot of flexibility.
Other interesting patterns can be done as well. Optional hooks can be defined as context methods. The flat namespace of the context object also acts like a facade, decoupling the consumer of API methods from the actual provider. By making every member of the context a function (as opposed to data members), late coupling is achieved, which I found very useful in simplifying initialization dependencies.
On the whole, I found the flat context to be extremely flexible, and developed a number of patterns for solving particular problems. I have an unfinished blog post on the topic that I may one day get around to.