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.