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.
ie
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
My idea on when to use redux/flux/reflux/anothingelseux and when to use context:
I would say in your case the -ux way is they way to go, there is no reason why your wrapper component should handle logic it has nothing to do with, and the code would be obscure. Imagine the dev going to your code later on and seeing that you receive this through the context. Any parent could have sent it, so he will need to check at where it has been sent. Just the same happens to your wrapper component, if similar operation start to multiply you will handle with many methods and handlers in it that have nothing to do there.
Having a store with action and reducers allows to separate the concerns in your case and would be the most readable way to do things.
Okay, so, wisdom probably says that using Redux and it's uni-directional data flow is the best solution. Therefore I have set the answer by @Mijamo as the answer.
I did end up creating the injectables solution that I was talking about in my post. So far it has been immensely useful. It's really really useful actually and I have already been able to produce some awesome stuff that would be too complicated using other techniques.
The best thing of all is that my injectable targets don't need to explicitly know about every possible component that will be injected in them.
I've been so happy with what I did that I created a library:
https://github.com/ctrlplusb/react-injectables
As you can see I try to make the component binding (target/source) as explicit as possible. You have to actually import and bind all respective targets in your code. This is helpful as you can get compile time checks (well sorta) for your target/source bindings. Much more helpful than magic string based bindings.
Anyways, it's probably still a crazy idea, but maybe I am crazy and that's why I love it so much. :)
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.