DEV Community

Tefoh
Tefoh

Posted on

HOC in vue 3 (higher order components)

lets talk about an advanced technique for reusing components, its a function will takes a component and give back new component. the idea comes from react community, here's the link . this idea maybe not so useful for every project but for third party libraries its great pattern.

init project

ok we will create project with vite because its fast. more details about vite. use this commands to create project

npm init @vitejs/app myapp
cd myapp
npm install
npm run dev

// or yarn 
yarn create @vitejs/app myapp
cd myapp
yarn
yarn dev
Enter fullscreen mode Exit fullscreen mode

we will create two components for counter CounterPlusOne.vue and CounterPlusFive.vue as the names will tell first component plus by one the counter, the second component plus counter by five. the components will be simple and look like this:

<!-- CounterPlusOne.vue -->
<template>
  <p>counter {{ counter }}</p>
  <button @click="increment">add</button>
</template>

<script>
export default {
  data: () => ({
    counter: 0,
  }),

  methods: {
    increment() {
      this.counter += 1;
    },
  },
};
</script>

<!-- CounterPlusFive.vue -->
<template>
  <p>counter {{ counter }}</p>
  <button @click="increment">add</button>
</template>

<script>
export default {
  data: () => ({
    counter: 0,
  }),

  methods: {
    increment() {
      this.counter += 5;
    },
  },
};
</script>

Enter fullscreen mode Exit fullscreen mode

as you can see things are similar in script part. the state, and maybe increment part if we could pass a prop to it. so we will extract this functionality to a function but gives us back the component we want.

HOC (higher order components)

the existing part is this function, but you have to know about render function and how it works in vue 3, the doc link . the summary of render function, its a vue function that create elements. and vue use it behind the scenes. but we dont have to know about it because we can use template in vue components. lets create a javascript file WithCounter.js and for now we just want to pass the component and give it back to us, simple and ez :)

import { h } from "@vue/runtime-core"

function WithCounter(WrappedComponent) {
  // we will return new component that render's WrappedComponent
  return {
    created() {
      console.log('HOC component created')
    },

    render() {
      return h(WrappedComponent)
    }
  }
}

export default WithCounter
Enter fullscreen mode Exit fullscreen mode

this is our first higher order component, maybe lots of things is new but i will explain. so first we have a function that accepts a component to render, remember the HOC component dont care about component it will render, it just make it resuable. our function will returns an object, this object is a new component, the components in vue is just objects in vue components you will export default an object in script part. render remains it will create a vNode with h function we imported at top. if we want to create a custom component with render function thats how we do it. to use this higher order component:

import CounterPlusOne from "./components/CounterPlusOne.vue";
import CounterPlusFive from "./components/CounterPlusFive.vue";
import WithCounter from "./components/WithCounter.js";

export default {
  components: {
    CounterPlusOne: WithCounter(CounterPlusOne),
    CounterPlusFive: WithCounter(CounterPlusFive),
  },
};
Enter fullscreen mode Exit fullscreen mode

for now it doesn't do much just renders the components and log HOC component created twice.

reusable HOCs

now we move counter functionality to our HOC. first start simple we just send a argument to with counter, means:

// App.vue
export default {
  components: {
    CounterPlusOne: WithCounter(CounterPlusOne, 1),
    CounterPlusFive: WithCounter(CounterPlusFive, 5),
  },
};

// WithCounter.js
function WithCounter(WrappedComponent, number)
Enter fullscreen mode Exit fullscreen mode

because its function we can pass as many arguments we want. lets move all duplicated code in this two components to WithCounter:


function WithCounter(WrappedComponent, number = 1) {
  return {
    data: () => ({
      counter: 0,
    }),

    methods: {
      increment() {
        this.counter += number;
      },
    },

    render() {
      return h(WrappedComponent, {
        counter: this.counter,
        increment: this.increment
      })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

as i said increment method can be refactored with given parameter. we only do pass counter and increment as props to components. so our component will be like this.

<!-- CounterPlusOne.vue -->
<template>
  <p>counter {{ counter }}</p>
  <button @click="increment">add</button>
</template>

<script>
export default {
  props: {
    counter: Number,
    increment: Function,
  },
};
</script>

<!-- CounterPlusFive.vue -->
<template>
  <p>counter {{ counter }}</p>
  <button @click="increment">add</button>
</template>

<script>
export default {
  props: {
    counter: Number,
    increment: Function,
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

summary

for now they totally same but its just an example, i use this pattern in my tdata package, that just sends form and get data from an api. i dont use this pattern a lot but i see its very useful for packages or third party libraries.

Top comments (0)