I have a proof of concept for this, which seems to work, but a part of me is wondering if this is really a good idea and if there is perhaps a better solution out there using something like Redux or an alternative strategy.
The Problem
Basically, I have a base React component for my entire application which has a bunch of typical components that you might expect, header, menu, footer etc etc.
Further down my tree (much further) I have a component for which it would be awesome if I could mount a new menu item for within my header component. The header component of course lives right at the top of my application so access is denied.
That is just one such example, but it's a problem case I have hit from many angles.
My Crazy Solution
I looked into using React's context in order to expose functions that would to allow child components to declare any additional elements they would like to appear within the header.
After playing around with the concept I eventually refactored it into a pretty generic solution that is essentially a React Element messaging system. There are three parts to this solution.
1. The Provider
Single instance component much in the same vein as Redux's Connect component. She's essentially the engine that receives and passes the messages along. Her basic structure (Context focused) is:
class ElementInjectorProvider extends Component {
childContextTypes: {
// :: (namespace, [element]) -> void
produceElements: PropTypes.func.isRequired,
// :: (namespace, [element]) -> void
removeElements: PropTypes.func.isRequired,
// :: (listener, namespace, ([element]) -> void) -> void
consumeElements: PropTypes.func.isRequired,
// :: (listener) -> void
stopConsumingElements: PropTypes.func.isRequired,
}
/* ... Implementation ... */
}
2. The Producer
A higher order component. Each instance can "produce" elements via the produceElements
context item, providing elements for a specific namespace, and then remove the elements (in case of component unmount) via removeElements
.
function ElementInjectorProducer(config) {
const { namespace } = config;
return function WrapComponent(WrappedComponent) {
class ElementInjectorConsumerComponent {
contextTypes = {
produceElements: PropTypes.func.isRequired,
removeElements: PropTypes.func.isRequired
}
/* ... Implementation ... */
}
return ElementInjectorProducerComponent;
};
}
3. The Consumer
A higher order component. Each instance is configured to "watch" for elements attached to a given namespace. It uses consumeElements
to "start" the listening via a callback function registration and stopConsumingElements
to deregister the consumption.
function ElementInjectorConsumer(config) {
const { namespace } = config;
return function WrapComponent(WrappedComponent) {
class ElementInjectorConsumerComponent {
contextTypes = {
consumeElements: PropTypes.func.isRequired,
stopConsumingElements: PropTypes.func.isRequired
}
/* ... Implementation ... */
}
return ElementInjectorConsumerComponent;
};
}
That's a rough overview of what I am intending on doing. Basically it's a messaging system when you look at it. And perhaps could be abstracted even further.
I already have redux in play, and guess what Redux is good for? So I can't help but feel that although this is working for me, perhaps it's not a good design and that I have inadvertently stood on Redux's toes or produced a general anti-pattern.
I guess the only reason I didn't jump straight into using Redux for this is is that because I am producing Elements, not simple state. I could go down the route of creating element descriptor objects and then pass that down through Redux, but that's complicated in itself.
Any words of wisdom?
UPDATE No 1
Some additional clarification on the above.
This allows me to inject Elements both up and down, and even left to right, on my full component tree. I know most React Context examples describe the injection of data from a Grandparent into a Grandchild component.
Also, I would want the above implementation to abstract away from the developer any knowledge of Context usage. In fact I would most likely use these HOFS to create additional wrappers that are specific to use cases and far more explicit.
i.e.
A consumer implementation:
<InjectableHeader />
A producer implementation:
InjectIntoHeader(<FooButton />)(FooPage)
It's pretty explicitly I think and easy to follow. I do like that I can create the button where it is most cared about which grants me the ability to create stronger relationships with it's peers.
I also get that redux flow is probably the right idea. It just feels like I make it a lot harder for myself - I can't help but think there may be some merit to this technique.
Is there any reason this is specifically a bad idea?
UPDATE No 2
Ok, I am now convinced this is a bad idea. I am basically breaking the predictability of the application and null'ifying all the benefits that a uni-directional data model provides.
I am still not convinced that using Redux is specifically the best solution for this case, and I have dreamt up a more explicit uni-directional solution that uses some of the concepts from above, without any context magic though.
I'll post any solution as an answer if I think it works. Failing that, I'll go Redux and kick myself for not listening to you all sooner.
Other examples
Here are a few other projects/ideas trying to solve the same(ish) problem using a variety of techniques:
https://joecritchley.svbtle.com/portals-in-reactjs
https://github.com/davidtheclark/react-displace
https://github.com/carlsverre/react-outlet