Description
With the arrival of the composition API, renderless components have become a bit less useful—IMO, composition functions are more versatile, easier to author, and easier to consume than renderless components.
That said, renderless components are still useful in some scenarios, and there are a few tricks to setting them up in Vue 3. Proper docs would explain the use case for renderless components, and have code examples for implementation in Vue. We can look to the React render props docs for inspiration.
There's also a question of where these docs should go. They definitely fit in with the Reusability & Composition section, but could also fit as one of the last guides in the Components In-Depth section.
I'll draft these docs in the near future, but meanwhile, below is some example code for anyone who might be looking. Here's an SFC playground for it.
Couple things to note in the code below:
- It's now possible to make renderless components without render functions, by rendering a slot as the only element in a Vue template. This wasn't possible in Vue 2, which forced you to use render functions.
- You can still use render functions to make renderless components, and the experience is improved by the
setup
function. Write all your component logic insetup
with the composition API, then return your render function fromsetup
at the end. - Render functions are not compatible with
script setup
, as far as I can tell. Probably not that much of a downside, since people using render functions are usually writing components in plain.js
or.ts
files instead of.vue
files.
Rendering a normal slot with a Vue template
<!-- Renderless_NormalSlot_Template.vue -->
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
const clickEffect = () => console.log('Page was clicked!')
onMounted(() => document.addEventListener('click', clickEffect))
onBeforeUnmount(() => document.removeEventListener('click', clickEffect))
</script>
<template>
<slot></slot>
</template>
Rendering a scoped slot with a Vue template
<!-- Renderless_ScopedSlot_Template.vue -->
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const count = ref(0)
const clickEffect = () => count.value++
onMounted(() => document.addEventListener('click', clickEffect))
onBeforeUnmount(() => document.removeEventListener('click', clickEffect))
</script>
<template>
<slot :count="count"></slot>
</template>
Rendering a normal slot with a render function returned from setup
<!-- Renderless_NormalSlot_RenderFn.vue -->
<script>
import { onMounted, onBeforeUnmount } from 'vue'
export default {
setup (props, { slots }) {
const clickEffect = () => console.log('Page was clicked by normal slot render function')
onMounted(() => document.addEventListener('click', clickEffect))
onBeforeUnmount(() => document.removeEventListener('click', clickEffect))
return slots.default
}
}
</script>
Rendering a scoped slot with a render function returned from setup
<!-- Renderless_NormalSlot_RenderFn.vue -->
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue'
export default {
setup (props, { slots }) {
const count = ref(0)
const clickEffect = () => count.value++
onMounted(() => document.addEventListener('click', clickEffect))
onBeforeUnmount(() => document.removeEventListener('click', clickEffect))
return () => slots.default({ count: count.value })
}
}
</script>