Reusing Logic with Scoped Slots

This article is based on my course Reusable Components

Scoped slots are an incredibly interesting yet often misunderstood feature in Vue.

One of my favourite things about them is how they let you reuse logic between components — but in a very unique way.

It hinges on this key insight which we'll return to and expand upon in a moment:

Scoped slots let you push logic into a child component, so it can be reused across multiple components.

But first we're going to look at reusability in Vue a bit more broadly.

The different tools of reusability

Vue gives us some great tools for reusing markup in our applications.

The first is the most obvious — components!

Chunks of code that we want to use over and over again can be packaged up into a component, instead of copy and pasting code all over the place.

We also get slots, which let us define markup in a parent and pass it down to a child component. With slots we don't care how the markup is used, only what the markup is. The child component is left to figure out what to do with the markup.

Reusing logic is a bit more complicated though.

Logic is harder to reuse

Over the years, Vue has gone through several different mechanisms to try and make logic easier to reuse.

Business logic that can be extracted as pure Javascript methods has always been straightforward to reuse. We can use the same techniques and patterns that we would normally use with code.

But with Vue-specific logic it's been a different story.

Code that interacts with the component directly isn't so easy to work with.

At first we had mixins, which we as a community (alongside the React community) realized wasn't the greatest solution. It often made code harder to understand, and modifying a mixin would often break components that relied on it.

Then we borrowed this idea of "higher order components" from the React community, and came up with the idea of renderless components.

Renderless components

These components are like mixins, but more tightly controlled. It's much easier to understand where methods are coming from, and how everything fits together.

Here's a quick example of using a renderless component to get the width of an element:

<template>
<ElementSize>
<template v-slot:default="{ width, height }">
<div>
This "div" is {{ width }}px wide
and {{ height }}px tall
</div>
</template>
</ElementSize>
</template>

The logic for getting the height and width of an element are encapsulated in the ElementSize component, so all we need to do is grab them using a scoped slot.

But renderless components still aren't that great. We're stuck inside of the template, so we have scoping issues.

We can't very easily use the height value here inside of a computed prop, since it only exists inside of the scoped slot.

Luckily, with Vue 3 we are now getting the composition API, which is a massive leap forward in terms of reusing logic. It's talked about a lot in other articles, so I won't say much about it here.

Instead I want to return to renderless components for a moment.

Why do renderless components work?

I was recently asking myself this question.

I mean, we use them to abstract logic, so that we can reuse it between components more easily.

But what is it about renderless components exactly that makes capable of doing this?

I was stumped on this one for awhile, but eventually I figured it out — it's all about the scoped slot.

Reusing logic with scoped slots

This is the key insight:

Scoped slots let you push logic into a child component, so it can be reused across multiple components.

Let's take the canonical scoped slot example of rendering a list.

First we start with a basic list without any slot:

<!-- Parent -->
<template>
<ul>
<li
v-for="item in list"
:key="item.id"
>
<span>{{ item.text }}</span>
</li>
</ul>
</template>

But let's add in a scoped slot, so that we can customize how this list is rendered:

<!-- Parent -->
<template>
<List>
<template v-slot:default="{ item }">
<span>{{ item.text }}</span>
</template>
</List>
</template>
<!-- List -->
<template>
<ul>
<li
v-for="item in list"
:key="item.id"
>
<slot :item="item" >
</li>
</ul>
</template>

You may have seen this example before (it's actually in the docs), but it's usually framed as letting the parent have control over how the list items are rendered.

This is certainly true — the parent in this example doesn't have to know about rendering the list, but can decide to make each item bolded:

<!-- Parent -->
<template>
<List>
<template v-slot:default="{ item }">
<span><strong>{{ item.text }}</strong></span>
</template>
</List>
</template>

But we're also pushing the logic of rendering the list into the child List component.

Now we can reuse this List component all over the place, and we don't have to rewrite our list rendering logic again.

But what about the Composition API?

Renderless components have their drawbacks, because scoped slots have inherent limitations when it comes to reusing logic in your Vue components.

So when should we use one or the other?

Scoped slots couple logic to the template, and simplify things when we're going to deal with HTML anyways.

You can write a List component like this or create a useList composition function. Both will let you reuse list related functionality.

If you're manipulating lists in computed props and other methods, you'll need to use the useList function. But if you're just rendering a list of items, the List component will probably feel more natural.

Conclusion

The composition API isn't the only good method we have to reuse logic in our components, we also have scoped slots.

Scoped slots let us push logic from one component into another. Once this logic is encapsulated in a child component, it can be more easily reused across our application in other components.

If you enjoyed this article, there is more to come with my course on Reusable Components.