With Vue 2, one way to create reusable logic was to use renderless components. After Adam’s blog post, the concept quickly became popular. But when Vue 3 introduced the composition API, which offers similar benefits at even greater flexibility, renderless components became less relevant.

But there are still some interesting things you can do with renderless components. When you combine them with dynamic components, for example.

<!-- DynamicSlot.vue -->

<template>
  <component :is="dynamic" />
</template>

<script setup>
const { dynamic } = useSlots();
</script>
<!-- ExampleUsage.vue -->

<template>
  <DynamicSlot>
    <template #dynamic>
      <p>This is the slot content</p>
    </template>
  </DynamicSlot>
</template>

Notice that we didn’t have to define the slot anywhere. But that alone is obviously not very impressive. We could have achieved the same thing using a regular named slot. Next, we’ll see what else we can do with this.

A generic data table

Let’s say we’re building a generic DataTable component. We want it to be used like this:

<!-- NameAndEmailTable.vue -->

<template>
  <DataTable :data="tableData">

    <DataColumn attribute="name">
      <template #header>
        Name
      </template>
      <template #cell="{ value: name }">
        {{ name }}
      </template>
    </DataColumn>

    <DataColumn attribute="email">
      <template #header>
        Email
      </template>
      <template #cell="{ value: email }">
        <a :href="`mailto:${email}`">
          {{ email }}
        </a>
      </template>
    </DataColumn>

  </DataTable>
</template>

We pass data to the DataTable component and define how the columns should be displayed through the DataColumn components. The DataColumn has slots for the column header and cell content. That’s a very flexible interface – we can easily create different column types by changing the slot contents. But there’s a problem: HTML tables are laid out rows-first. You nest columns inside rows, not the other way around.

<table>
  <tr>
    <td>
      Rows first, columns later.
    </td>
  </tr>
</table>

But with our interface, we want to define the columns first and then iterate over the column definitions to render the table rows. How can we get this to work? With renderless slots and dynamic components – obviously.

Teleporting renderless slots

The idea is that the renderless DataColumn component “teleports” its slot contents to the DataTable parent. The DataTable then uses dynamic components to render the slot contents into the appropriate rows.

<!-- DataTable.vue -->

<template>
  <table>
    <!-- header row-->
    <tr>
      <th v-for="col in columns">
        <component :is="col.headerSlot" :column="col" />
      </th>
    </tr>
    <!-- body rows -->
    <tr v-for="row in data">
      <td v-for="col in columns">
        <component :is="col.cellSlot" :value="row[col.attribute]" />
      </td>
    </tr>
  </table>
</template>

<script setup>
import { ref, provide } from 'vue';

defineProps({
  data: {
    type: Array,
    required: true,
  },
});

const columns = ref([]);

provide('registerColumn', (col) => {
  columns.value.push(col);
});
</script>
<!-- DataColumn.vue -->

<template />

<script setup>
import { inject, useSlots } from 'vue';

const props = defineProps({
  attribute: {
    type: String,
    required: true,
  },
});

const slots = useSlots();

const registerColumn = inject('registerColumn');

onMounted(() => {
  registerColumn({
    attribute: props.attribute,
    headerSlot: slots.header,
    cellSlot: slots.cell,
  });
});
</script>

We use provide and inject to register the column definitions with the DataTable. Each column definition includes two renderless slots: a headerSlot and a cellSlot. We can render those slots wherever we want, which allows us to “pivot” the columns and render them rows first. Notice, that we can also pass data to the slots. It behaves exactly like a regular scoped slot.

Conclusion

I usually choose a composable over a renderless component, because being able to access it outside the template makes it more flexible. Nonetheless, renderless components still have their right to exist. Used creatively, they can help us to create truly reusable components.