Vue: Discussion: Best way to create a HOC

Created on 25 Jul 2017  ·  40Comments  ·  Source: vuejs/vue

Version

2.4.2

Reproduction link

https://jsfiddle.net/o7yvL2jd/

Steps to reproduce

I've searching for the right way to implement HoC with vue.js. But couldn't found any suitable example.
The link below is known HoC implementations. But didn't work expected.
https://jsfiddle.net/o7yvL2jd/

How can I implement HoC with vue.js?
I just want to know how to implement HoC in the react.js way.

What is expected?

They are HoCs that simply renders components passed in as parameters.
HoCs containing slots and events will render normally.

What is actually happening?

The element to be rendered is missing, or the rendered order differs from the baseComponent.
Some HoC implementations do not work with event handlers.

discussion

Most helpful comment

Okay, so I played around with a way to make this easier.

Have a look here: https://jsfiddle.net/Linusborg/j3wyz4d6/

I'm not happy with the API as it's a very rough idea sketch, but it's able to do anything it should be able to do.

import { createHOC, createRenderFn, normalizeSlots } from 'vue-hoc'
const Component = {
  props: ['message']
  template: `<p>{{message}}</p>`
}
const HOC = createHOC(Component)

createHOC will take care of:

  • copying the props from the component
  • swapping $createElement witht that from the parent for proper slots resolving.
  • add a name
  • if no second argument is provided (as in the above example), it renders the component and passes on:

    • props

    • attrs

    • listeners

    • normal slots

    • scoped slots

That on it's own isn't very useful, of course. So the fun happens in second argument which is a simple Component object.

If you want to write the render function on your own, you of course can. If you just want to extend props, attrs or listeners, you can use the createRenderFn helper. it will create a render function like the default one described above but will merge any attrs, props or listeners you pass to it with those from the parent.

const HOC = createHOC(Component, {
  mounted() { 
    console.log(`lifecycle hooks work, it's simply component options`) 
  },
  render: createRenderFn(Component, {
    props: {
      message: 'Hello from your HOC!'
    }
  }
})

If you want to write your own render function, you can use the normalizeSlots helper to transform the object from this.$slots into a proper array to pass on:

const HOC = createHOC(Component, {
  render(h) {
    return h(Component, {
      props: { message: 'hi from HOC'}, // nothing from parent will be passed on
    }, normalizeSlots(this.$slots))
  }
})

Comments wanted :)

All 40 comments

Hello @eu81273

Thank your for your interest in this project.

However, your issue is a usage/support question, and the issue tracker is reserved exclusively for bug reports and feature requests (as outlined in our Contributing Guide).

We encourage you to ask it on the forum , Stack Overflow or on our discord chat and are happy to help you out there.

FWIW, I took a look out of personal interest - this should work 100% of cases.

const HOC = WrappedComponent => ({
  props: typeof WrappedComponent === 'function' // accept both a construtor and an options object
    ? WrappedComponent.options.props 
    : WrappedComponent.props,
  render (h) {
    // reduce all slots to a single array again.
    const slots = Object.keys(this.$slots).reduce((arr, key) => arr.concat(this.$slots[key]), []);
    return h(WrappedComponent, {
      attrs: this.$attrs,
      props: this.$props,
      on: this.$listeners,
    }, slots);
 }
});

I edited HOC04 in your example since it was the closest to the solution:

https://jsfiddle.net/Linusborg/o7yvL2jd/22/

Edit: still an issue with slots, investigating ...

I might have solved it: https://jsfiddle.net/BogdanL/ucpz8ph4/. Slots are just hardcoded now, but that's trivial to solve.

Seems the solution is along the method of @lbogdan but createElement should have a way of taking slots, just like it can take scopedSlots.

However, it is _still_ a lot of effort to create an HoC. There's a lot to remember to pass through, while with react you just render the WrappedComponent with props.

I just thought of a very simple solution...let me know if I'm missing something here:

const HOC06 = WrappedComponent => Vue.extend({
   mounted () {
      console.log('mounted 6')
   },
   ...WrappedComponent
})

Based on the examples given by @LinusBorg and @lbogdan, the most minimal HoC implementation that can handle components with slots is:

const HoC = WrappedComponent => ({
    props: typeof WrappedComponent === 'function' 
        ? WrappedComponent.options.props 
        : WrappedComponent.props,
    render (h) {
        const slots = this.$slots;
        const scopedSlots = {};
        Object.keys(slots).map(key => (scopedSlots[key] = () => slots[key]));

        return h(WrappedComponent, {
            attrs: this.$attrs,
            props: this.$props,
            on: this.$listeners,
            scopedSlots,
        });
     }
});

As @blocka mentioned, it is still a lot of effort to create an HoC with vue.js.

@eu81273

const HOC06 = WrappedComponent => Vue.extend({
   mounted () {
      console.log('mounted 6')
   },
   ...WrappedComponent
})

This seems to pass your test, no? Of course, you would have to adjust it depending on whether WrappedComponent is a constructor or object, but no need to pass slots, events or props.

it is still a lot of effort to create an HoC with vue.js.

Apart from the issue with slots, this is just due to the fact that Vue does have a more complex API than React, which in this scenario is a disadvantage. I admire Reacts minimal API in these kinds of use cases - Vue was just designed with slightly different deign goals, so HOCs don't come as easily as in React.

But it should be fairly trivial to create a createHOC() helper function that wraps this initial setup for you, shouldn't it?

Well, it really depends what the end goal is. From what I understand, the goal of HoC is to somehow change (decorate) the original component (WrappedComponent) to add (or inject) props, methods, event listeners etc. (much like a mixin, really :smile: ). HOC06 variant only changes the component definition, it doesn't change the way in which it's instantiated.

@blocka The goal of HOCs often is to get state (e.g. from redux / vuex) and inject it into the wrapped component's props - that would not work with your approach.

@LinusBorg right. I knew it was too good to be true and that I was forgetting something obvious.

I think this a good example of implementing a real use case HoC in Vue: https://github.com/ktsn/vuex-connect.

Vue Hocs would be an awesome plus (since it's almost always brought up in any vue vs react debate). Perhaps an official repo could be created to develop a vue-hoc-creator package? That way we could work on a robust, supported, implementation

Btw, got a better way: use $createElement from the parent component instead of the HOC's own - this make the child resolve the slots correctly:

https://jsfiddle.net/o7yvL2jd/23/

Cute, but all the more reason for there to be some official tool so we
don't all keep on reinventing this code.

On Sun, Jul 30, 2017 at 4:33 PM, Thorsten Lünborg notifications@github.com
wrote:

Btw, got a better way: use $createElement from the parent component
instead of the HOC's own - this make the child resolve the slots correctly:

https://jsfiddle.net/o7yvL2jd/23/


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/vuejs/vue/issues/6201#issuecomment-318927628, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AACoury4Fix2jsX_lyTsS6CYOiHJaOuVks5sTOiugaJpZM4Oh0Ij
.

I'm sorry for not coming up with an official solution yet. Glad you think it's cute though.

My solution is not perfect either, there are other problems to sort out - i.e. scoped slots won't work with my latest trick.

edit: oh, never mind, they work

An official solution will probably be done, at least I would expect so - but it has to be thorougly thought through and tested.

Okay, so I played around with a way to make this easier.

Have a look here: https://jsfiddle.net/Linusborg/j3wyz4d6/

I'm not happy with the API as it's a very rough idea sketch, but it's able to do anything it should be able to do.

import { createHOC, createRenderFn, normalizeSlots } from 'vue-hoc'
const Component = {
  props: ['message']
  template: `<p>{{message}}</p>`
}
const HOC = createHOC(Component)

createHOC will take care of:

  • copying the props from the component
  • swapping $createElement witht that from the parent for proper slots resolving.
  • add a name
  • if no second argument is provided (as in the above example), it renders the component and passes on:

    • props

    • attrs

    • listeners

    • normal slots

    • scoped slots

That on it's own isn't very useful, of course. So the fun happens in second argument which is a simple Component object.

If you want to write the render function on your own, you of course can. If you just want to extend props, attrs or listeners, you can use the createRenderFn helper. it will create a render function like the default one described above but will merge any attrs, props or listeners you pass to it with those from the parent.

const HOC = createHOC(Component, {
  mounted() { 
    console.log(`lifecycle hooks work, it's simply component options`) 
  },
  render: createRenderFn(Component, {
    props: {
      message: 'Hello from your HOC!'
    }
  }
})

If you want to write your own render function, you can use the normalizeSlots helper to transform the object from this.$slots into a proper array to pass on:

const HOC = createHOC(Component, {
  render(h) {
    return h(Component, {
      props: { message: 'hi from HOC'}, // nothing from parent will be passed on
    }, normalizeSlots(this.$slots))
  }
})

Comments wanted :)

@LinusBorg Very nice!

What I think would help is coming up with real life HoC use cases and solving them using these primitives.

Absolutely.

I'm mentioning this issue (EDIT (mybad): https://github.com/vuejs/vuejs.org/issues/658).
Since you're using the undocumented $createElement API, It would be worth documenting it for plugin developers.

Your link is wrong (unless you really wanted to link to an issue from 2014)

But yes, technically the $ceateElement API is still missing on the API page.

@AlexandreBonaventure That issue is from vue 0.x days. :smile:
Also, createElement is documented here: https://vuejs.org/v2/guide/render-function.html#createElement-Arguments .

The function is documented as an argument of the render function, but not that it is available via this.$createElement. There's an open issue for that on vuejs/vuejs.org/issues/658

@LinusBorg But it's basically the same function that gets sent to the render() function, right?

exactly the same. It's just not documented clearly that it's also available outside of the render function via this instance method.

I'm just having a play with the above example and there are a couple of issues when using it in a more complex scenario, hence the need for an official repo. A couple of notes:

  • createRenderFn should check if attrs/props/listeners are functions and evaluate them, this would allow you to dynamically set props etc. based on existing props.
  • for composability should component be the second parameter, and if the entire createHOC method was curried we could easily chain multiple hoc creators together.
  • because the hoc is added as a mixin, if you try to chain 2 hocs together (i.e. withDefaultProps(withProps(component, {}), {}) the second hoc does not have access to the parent's props example

Hey folks, I've been looking at writing an implementation of this.
https://github.com/jackmellis/vue-hoc/tree/develop
Let me know what you think and I'll look to publish it soon. I'm also thinking about writing a recompose-style package.

A recompose style package would be great. Really could have used something
like withState recently.

On Sat, Aug 19, 2017 at 5:50 AM, Jack notifications@github.com wrote:

Hey folks, I've been looking at writing an implementation of this.
https://github.com/jackmellis/vue-hoc/tree/develop
Let me know what you think and I'll look to publish it soon. I'm also
thinking about writing a recompose-style package.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/vuejs/vue/issues/6201#issuecomment-323513287, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AACoumQWQMgpVeIvzJ3Ti8A4kLBD04Hhks5sZq_zgaJpZM4Oh0Ij
.

@jackmellis Great that you take a lead on this :)

A couple of thoughts on your feedback you gave previously:

  • I think the curried version is a great point and should be the "default" in a way, as that'S how HOCs are generally done, aren't they?
  • Good point about the problem with mixins. Do you already have an idea how to mitigate this? I don't have one at this moment, but my gut feeling is that this should be mitigatable in a combination of the curried variants you created and using Vue.config.optionsMergeStrategies

I also thought about naming. I don't like createRenderFn, something like renderComponentWith would more meaningful and make more sense in a scenario where we embed it in some other nodes:

render(h) {
  return h('DIV', {staticClass: 'some-styling' }, renderComponentWith({ props: {... } }))
}

  • In the end I've opted for both. So createHOC is component-first non-curried, and then there's a curried variant createHOCc. The React ecosystem is very functional-focused, unlike Vue, which acts more like an OOP framework. So I think it's best to keep consistent with the rest of Vue, but still offer a functional alternative.
  • I've just added some code to deal with this. Rather than having the whole hoc as a mixin, I manually merge the hoc and the options together using the optionsMergeStrategies. The only problem with that is that optionsMergeStrategies expects a vm as the last parameter, but I'm doing the merging before instantiation, so there is no vm.
  • I'm quite happy with createRenderFn since that's exactly what it's doing. But the more I put this together, the less I think people will use it directly. The general usage will be something like:
const hoc = createHOC(
  Component,
  {
    created(){
      /* ... */
    }
  },
  {
    props: (props) => ({
      ...props,
      someProp: 'foo'
    })
  }
);

Perhaps I don't quite understand your example?

Perhaps I don't quite understand your example?

No, I think you are on the right track, my thoughts were going in a similar direction when I thought some more about it after my last reply.

In my initial POC I included it so people could add additional markup around the rendered component, but that would mean it's not really a HOC anymore, as it would bring in UI as well...

Yes I think you are attempting to render additional content, you've gone outside of HOC territory and might as well create an SFC to handle it.

I've just published vue-hoc on npm!

I've also been working on vue-compose which is a quick documentation-session-away from being ready as well. Although it's similar to recompose, Vue handles a lot of clever stuff (like caching computations and it encourages the use of this) so it actually doesn't need quite as complex (or functional-style) syntax.

extends works differently, but can be used to achieve similar results, that's true.

I will close this since I think @jackmellis project does provide a solid foundation. We don't intend to include a way to create HOC's into core, mainly because we don't see a large benefit over mixins / Extends.

mainly because we don't see a large benefit over mixins / Extends.

@LinusBorg React have already abandoned mixin, HOC brings lots of benefit.

This article have analyzed the benefits:

  • The contract between a component and its mixins is implicit.
  • As you use more mixins in a single component, they begin to clash.
  • Mixins tend to add more state to your component whereas you should strive for less.
  • ....

I think Vue team should consider to support HoC more carefully....although it is not easy(It seems Vue is not designed in this way).

I'm not convinced that HoCs are such a superior concept. The "implicit contract" potential clashes can happen with HoCs just as well, for example.

See this talk By the maintainer of React-router about it: https://www.youtube.com/watch?v=BcVAq3YFiuc

That being said I don't think they are bad either, they are useful in many situations. They are just not the magic bullet they are praised as in the React world, though, they have their own downsides.

As being evident from the discussion above, implementing HoCs in Vue isn't as trivial as in React because Vue's API and functionality is broader and more edge cases have to be taken care of.

We can surely talk about how to improve this as long as it doesn't require breaking anything in Vue - HoC's aren't worh a breaking change in my view.

666

the most minimal HoC implementation that can handle components with slots is:

function hoc(WrappedComponent) {
  return {
    render(h) {
      return h(WrappedComponent, {
        on: this.$listeners,
        attrs:this.$attrs,
        scopedSlots: this.$scopedSlots,
      });
    },
  };
}

comparing to replys above,

  • props config is not need, which could be passed from this.$attrs to child
  • extra slots is not need, this.$scopedSlots contain slots since v2.6.0+

i writed a example to check

Was this page helpful?
0 / 5 - 0 ratings