How to Use Nested Slots in Vue (including scoped slots)

I recently figured out how to implement nested slots recursively, including how to do this with scoped slots.

It all started when I wanted to see if I could build a component that replicated the v-for directive, but only using the template.

To make this story short: I did.

(follow me to get my tweets as I learn new stuff!)

And yes, it supports slots and scoped slots — and could be made to support named slots as well.

We can use it like this:

<template>
<div>
<!-- Regular list -->
<v-for :list="list" />
<!-- List with bolded items -->
<v-for :list="list">
<template v-slot="{ item }">
<strong>{{ item }}</strong>
</template>
</v-for>
</div>
</template>

The first one will print out the list of items normally, while the second one will wrap each item in a <strong> tag.

It's not a very useful component, but I like to have fun experimenting as I find I learn the most that way.

Let's take a look at what's going on here.

Looping without loops

Normally when we'd want to render out a list of elements or components we'd use the v-for directive. But we want to get rid of it completely.

So, how do we render out a list of items without using a loop?

Any guesses?

The answer is: recursion.

We can use recursion to represent a list of items.

It's actually not too complicated. I'll show you how this works.

Representing a list recursively

One of my favourite courses in university was Programming Language Concepts.

The most interesting part of it to me was exploring functional programming and logic programming, and seeing the differences to imperative programming (Javascript and most popular languages are imperative).

This was the course that really opened my eyes on how to use recursion, since with pure functional languages everything is recursion.

Anyways, from that course I learned that you can represent a list recursively.

Instead of using an array, each list is a value (the head), and another list (the tail).

[head, tail]

So if you wanted to represent the list [1, 2, 3], it would be represented recursively as:

[1, [2, [3, null]]]

We have to end the list somehow, so we use null instead of another array (an empty array would work too).

Maybe you're starting to see where I'm going with this...

We can use this concept and apply it to our components. Instead, we'll be nesting our components recursively in order to represent our list.

We'll end up rendering something like this. Notice the nested structure of our "list":

<div>
1
<div>
2
<div>
3
</div>
</div>
</div>

Admittedly, this isn't the exact same thing as what v-for would render, but that's not exactly the point of this exercise either.

So now that we've worked out what we're doing, how do we create it?

First, I'd like to challenge you to try building it yourself before moving on. I think you'll be able to figure it out, and you'll learn more that way than by just reading how I did it.

Okay, now it's time for me to show how I solved this problem.

Building the component

First, we'll tackle rendering out the list of items recursively. Once we get that working, we'll come back and figure out how to make the slots work.

Using recursion to render lists

Instead of using the recursive list we showed before, we'll work with a plain array:

[1, 2, 3]

We have to cover 2 cases here:

  1. Base case - rendering the first item in the list
  2. Recursive case - rendering the item, and then the next list

Let's pass [1, 2, 3] to v-for:

<template>
<v-for :list="[1, 2, 3]" />
</template>

We want to grab the first item in the list, the 1, and display it:

<template>
<div>
{{ list[0] }}
</div>
</template>

Now the component will render out 1, like we expect it to.

But we can't just render out the first value and stop. We need to render out the value, and then render out the rest of the list as well:

<template>
<div>
{{ list[0] }}
<v-for :list="list.slice(1)" />
</div>
</template>

Instead of passing in the whole list array, we chop off the first item and pass down that new array. We've already printed out the first item, so no need to keep it around.

This is the sequence of things that are happening here:

  1. We pass [1, 2, 3] into v-for to render
  2. Our v-for component renders 1, and then passes [2, 3] into the next v-for to render
  3. Which takes the [2, 3] and renders the 2, and then passes [3] into the next v-for
  4. This last v-for component then renders out the 3, and we've printed out our list!

The structure of our Vue app looks like this now:

<App>
<v-for>
<v-for>
<v-for />
</v-for>
</v-for>
</App>

You can see that we have several v-for components all nested within each other!

One last thing we need to do here though. We need to stop the recursion!

<template>
<div>
{{ list[0] }}
<v-for
v-if="list.length > 1"
:list="list.slice(1)"
/>
</div>
</template>

Eventually we run out of items to render, so we need to stop our recursion (or we'll get tons of errors and our computer will melt).

Our next step is get slots working.

Recursively nested slots

We've got the basic component working, but we also want it to work with scoped slots, so we can customize how we render each item:

<template>
<v-for :list="list">
<template v-slot="{ item }">
<strong>{{ item }}</strong>
</template>
</v-for>
</template>

It's not much extra work to get slots in here, so we'll do that right now!

Nesting slots

Once I figured out how to nest slots recursively, it set off this obsession with slots — which I'm still working through 😅

If you're looking for some more advanced content on slots, I also did a deep dive into case study that looks at different ways of approaching an architecture problem in Vue with slots.

First we'll take a quick detour into how nested slots work, then we'll show how to incorporate them into our v-for component.

If you know how to use slots normally, I think you'll get this pretty quick.

Let's say we have three components, a Parent, a Child, and a Grandchild. We want to pass some content from the Parent component and have it rendered in the Grandchild.

All we're doing is passing the slot from the Parent to the Child like we normally do, and then again from the Child to the Grandchild.

Starting with the Parent, we pass in some content:

// Parent.vue
<template>
<Child>
<span>Never gonna give you up</span>
</Child>
</template>

We do some things in our Child component — which we'll get to in just a moment. And then our Grandchild component takes the slot and renders out the content:

// Grandchild.vue
<template>
<div>
<slot />
</div>
</template>

So what does this Child component look like?

We need it to take the content from Parent and provide it to the Grandchild component, so we're connecting two different slots together here:

// Child.vue
<template>
<Grandchild>
<slot />
</Grandchild>
</template>

Remember, the <slot /> element renders out content that's passed to the component as a slot. So we're taking that content from Parent and then rendering it inside of the slot of Grandchild.

Adding in our scoped slots

The only thing different with nesting scoped slots is that we also have to pass along the scoped data.

Adding this into our v-for we now get this:

<template>
<div>
<slot v-bind:item="list[0]">
<!-- Default -->
{{ list[0] }}
</slot>
<v-for
v-if="list.length > 1"
:list="list.slice(1)"
>
<!-- Recursively pass down scoped slot -->
<template v-slot="{ item }">
<slot v-bind:item="item" />
</template>
</v-for>
</div>
</template>

Let's look at our base case first.

If no slot is provided, we default to what's inside of the <slot> element, and render list[0] just like before. But if we do provide a slot, it will render it out and pass the list item to the parent through the slot scope.

The recursive case here is similar. If we pass in a slot to v-for, it will render it within the slot of the next v-for, so we get our nesting. It also grabs the item from the scoped slot and passes that back up the chain too.

And now, we have a component that imitates (more or less) a v-for, but only using the template!

Wrapping up

We went through a long and winding path to get here, but now we've seen how to create a component that replicates a v-for, but only using the template.

We covered:

  • representing lists recursively
  • recursive components
  • nested slots and nested scoped slots (or recursive slots if prefer calling them that)

If you enjoyed this article, please share it with others who may enjoy it as well!

And sign up for my email list below if you want to get advanced Vue content like this every week.