vuejs / docs

📄 Documentation for Vue 3
https://vuejs.org
Other
2.84k stars 4.21k forks source link

Add docs for renderless components #1331

Open AlexVipond opened 2 years ago

AlexVipond commented 2 years ago

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:

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>
skirtles-code commented 2 years ago

When I was adding the section about functional components to the docs for render functions, I did initially have a short section talking about renderless components. For various reasons I removed it from the draft that got merged.

I would class renderless components as a usage pattern, rather than as a core feature. They're also not something that beginners need to learn in their first few hours of using Vue. In my opinion, they would be a good cookbook topic.

Currently the docs are being rewritten on the next branch. I imagine it'll be another month or so before that's ready, but we're avoiding making any significant changes to the existing docs until that switchover is complete. That said, if you want to work on some draft material for this topic then feel free, just be aware that it probably can't be merged until the new docs are ready.

AlexVipond commented 2 years ago

Yeah, that makes sense to me. I'll draft something up, but wait until next is ready for PRs.

Probably higher priority: I'll also draft an addition to the setup function docs, so that the render function section includes an example of using context.slots to render a slot. It was unclear to me and appears to be unclear to other people that context.slots[slotName] is a render function that returns an array of VNodes, which means it can be returned from setup instead of () => h(...).

skirtles-code commented 2 years ago

context.slots[slotName] is similar to a render function but it is not a render function. It does return an array of VNodes but it takes different arguments.

Even if your slot doesn't care about the arguments it's passed, you also risk losing rendering updates if you return it as a render function from setup. So, for example, this won't work reliably:

setup (props, { slots }) {
  return slots.default
}

It may appear to work but it will fail if the slot function is replaced during parent re-rendering. In the example below, notice how the v-if gets ignored, because the child is using the initial value of slots.default as its render function and not the current value:

Running demo

To get this to work correctly, as well as to ensure the slot is passed appropriate arguments, we would need to wrap it in an actual render function:

setup (props, { slots }) {
  return () => slots.default?.()
}

If you want to add a note about returning a slot without using h, I think it might be better to put it in here:

https://v3.vuejs.org/guide/render-function.html#return-values-for-render-functions

It could follow on from the part about returning an array.