In this article we’re going to learn how to create renderless components in Vue, and how and when to use them.
Renderless components are components that don’t output any sort of HTML to the DOM by themselves. They are very unique, because they serve as logic wrappers that you can put into your template, and pass in a custom piece of HTML into them. They will perform their logic and inject it into your HTML via something called a scope.
In this article we’re going to learn how to create Renderless components in Vue, and how and when to use them. Be aware that due to the scope (hah!) of the article, I will have to assume that you have previous basic knowledge about Vue slots and scoped slots.
If you need a refresher on scoped slots, you can check out the documentation for scoped slots here, or take a look at these blog posts on Vue slots and Vue scoped slots.
If you ever find yourself writing a component that has a particular logic inside of it, but wish that the user of this component could write any custom HTML for it and make use of this logic—then renderless components could be one of your solutions.
Note that I said one of your solutions because this can also be achieved by writing a mixin, or even a standard JS class or function that injects this behavior into your components. In fact, I would argue that most of the time a functional approach will be superior in any way or form—think in terms of Vue 3 and the composition API, resuable and encapsulated code that can be injected and used in any of your components.
Regarding mixins in particular, remember that they pose the disadvantage of potential collision with other parts of your component. In comparison to that particular disadvantage, renderless components do have the lead due to their encapsulation of logic.
Now, having said that, there are scenarios where you may want to approach the problem with a renderless component simply because you want to display this behavior in the DOM, or because you’re writing a component library that needs to be super flexible.
In this article we’re going to look at a basic example that will demonstrate this last scenario. Imagine that you are writing a component that has the ability to sort an array of objects by a particular property—but you don’t want to be strict about how this content should be rendered.
Perhaps the user wants to put it in an <ol>
, or maybe even make a <table>
. In this case, a renderless component may be a good solution.
To get started with a renderless component, let’s look at the most basic setup one could have, and how we could use it on another component.
A renderless component has no <template>
because it doesn’t output anything to the DOM. It does, however, have a render function that exposes a single scoped slot. That way, anything we toss at it will be rendered in the parent’s template—normal slot behavior.
First things first, let’s create our renderless list-ordering component. We’re going to call it OrderedObjects because it seriously takes me longer to name things than it does to write the actual code and I just gave up and named it this—please bear with me. #developerlife
# OrderedObjects.vue
<script>
export default {
render() {
return this.$scopedSlots.default({});
}
};
</script>
As I mentioned before, the only real requirement here is that we return a single scopedSlot, default. The {} in the function is where we’re going to expose data to the parent later on, but don’t worry about it for now.
Let’s go back to the App.vue or wherever you are putting your actual components, and use this component. Remember to import it and add it to components: {} first!
# App.vue
<template>
<div id="app">
<OrderedObjects>Hi!</OrderedObjects>
</div>
</template>
<script>
import OrderedObjects from "./components/OrderedObjects";
export default {
components: {
OrderedObjects
}
};
</script>
If you run this code on your browser right now, you will only see the Hi! string being output to the string, which means that the scopedSlot is doing its work!
Next, let’s create some dummy data to play with and pass it down to OrderedObjects. We’re first going to create the data in App.vue.
# App.vue
<template>
<div id="app">
<OrderedObjects :objects="stuffs">Hi!</OrderedObjects>
</div>
</template>
<script>
import OrderedObjects from "./components/OrderedObjects";
export default {
components: {
OrderedObjects
},
data() {
return {
stuffs: [
{ name: "some", importance: 2 },
{ name: "stuffs", importance: 1 },
{ name: "and", importance: 1 },
{ name: "things", importance: 0 },
{ name: "Goku", importance: 9001 }
]
};
}
};
</script>
First, we added stuffs to the data() object of our parent and pushed some dummy data into it. Finally, make sure you add :objects=“stuffs” to the actual OrderedObjects element in the template. We’re going to create a property objects inside it right away.
# OrderedObjects.vue
<script>
export default {
props: {
objects: { type: Array, required: true }
},
render() {
return this.$scopedSlots.default({});
}
};
</script>
Now that we’ve added an objects prop to our OrderedObjects component, we can actually make some use of it. This component is supposed to be ordering things for us, but for now, let’s just return the list to the parent as it was given to us.
Add the objects property to the scopedSlot object like follows.
# OrderedObjects.vue
<script>
export default {
props: {
objects: { type: Array, required: true }
},
render() {
return this.$scopedSlots.default({
objects: this.objects
});
}
};
</script>
If you check your browser right now, nothing will have changed yet. This is because we haven’t yet made use of our exposed data on our parent. Let’s go back to App.vue and make the following changes.
# App.vue
<template>
<div id="app">
<OrderedObjects :objects="stuffs">
<template v-slot:default="{objects}">
<ul>
<li v-for="obj in objects" :key="obj.name">
{{ obj.importance }} - {{ obj.name }}
</li>
</ul>
</template>
</OrderedObjects>
</div>
</template>
If you go back to your browser, you should see that now we have a list of items being displayed in the screen. Remember this object that we passed down with the objects property in our render function in the last part?
{
objects: this.objects
}
This is exactly what we are getting back from the scoped slot in this line, the object with the objects key in it. We then use JavaScript destructuring to unpack it.
<template v-slot:default="{objects}">
Right now we’re not doing a lot inside of OrderedObjects with our data, and just passing it back seems like a wasted opportunity, like having 🥑 with no toast. So let’s modify our component to actually reorder our data by name.
# OrderedObjects.vue
<script>
export default {
props: {
objects: { type: Array, required: true }
},
render() {
return this.$scopedSlots.default({
objects: this.orderedObjs
});
},
computed: {
orderedObjs() {
const objs = [...this.objects];
return objs.sort((a, b) => {
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
return 0;
});
}
}
};
</script>
What we’ve done here is first created a computed property called orderedObjs. Inside this computed property we make a copy of the this.objects array (if you skip this step, you’ll be modifying the prop, which is a big NO!).
We then apply a sort function to the copy of the array which simply evaluates the name property and arranges the order of the items.
Finally, we use this new computed property in our render function. Instead of passing into the scoped slot the this.objects prop, we pass back this.orderedObjs.
Check out your browser now, and you should see that the data in the list is now ordered by name!
Now that you know how to create a renderless component and how it works, let’s create a second way of rendering this list so that the real utility of these components is showcased better.
Go back to App.vue and add the following code:
# App.vue
<template>
<div id="app">
<OrderedObjects :objects="stuffs">
<template v-slot:default="{objects}">
<ul>
<li v-for="obj in objects" :key="obj.name">{{ obj.importance }} - {{ obj.name }}</li>
</ul>
</template>
</OrderedObjects>
<OrderedObjects :objects="stuffs">
<template v-slot:default="{objects}">
<table>
<tr v-for="obj in objects" :key="obj.name">
<td>{{ obj.importance }}</td>
<td>{{ obj.name }}</td>
</tr>
</table>
</template>
</OrderedObjects>
</div>
</template>
Notice the second use of OrderedObjects. We are feeding the exact same data into it, the stuffs array into the objects property. However, notice that this time we are actually going to display our data in a table (🤢 I know).
Thanks to the power of scoped slots and the processing of the data that our renderless component is encapsulating, we can now have a wrapping component that modifies, parses, or even hits an API for us to parse data. All with the flexibility to allow the users of this component to pass down their own HTML to display the results as they see fit!
The code for this article can be found on the following sandbox: https://codesandbox.io/s/renderless-components-prqmt
Renderless components are just one way to achieve an encapsulation of shareable, or reusable code. They solve the specific problematic of wanting to have this share-ability directly on your template, but can also be replaced via the solutions discussed in the beginning of this chapter.
Regardless, it’s a great tool to know (and understand!) in the Vue tool belt!
As always, thanks for reading and share with me your renderless component experiences on Twitter at: @marinamosti.
P.S. All hail the magical avocado! 🥑
P.P.S. ❤️🔥🐶☠️
Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.