Using react's context to allow rendering of a

2019-07-17 06:49发布

问题:

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

回答1:

My idea on when to use redux/flux/reflux/anothingelseux and when to use context:

  • -ux stores are useful to store information shared between component in a transversal way. This is typically your use case: communicating between components that don't have any other obvious connexion and that are far away from each other in the tree.
  • Context is useful to provide information to children when you don't know where they will be or how many of them will need them. For example I use context for a map to provide information to its children on the current viewport. I don't know if all the children will use it but they are all potentially interested and they are not supposed to change it.

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.



回答2:

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. :)