The pain with Vue template
One of the most common bummers with templates comes from inability to define a template part and reuse it across template, for example when some block needs to appear in different places in the markup depending on some condition.
It doesn’t always make sense to extract a part into a separate component of it’s own. Even with a separate component it’s quite common that they need to be passed numerous props and simply duplicating props declaration multiple times is too much of a repetition that grows your template and leaves you open to potential to introduce a bug: when a new prop needs to be added to all of the occurrences it’s easy to overlook one (and then debug it for half hour 🥲).
Let’s consider a case where you need to render some template block in header or footer depending on whether view is compact. This is not a problem in React where you can easily do something like this:
const block = (
<div
class="block"
onClick="handleClick"
<!-- here may go other prop definitions, perhaps complex ones -->
>
Block that should be reused
</div>
);
return (
<div class="container">
<div class="header">
Header content
{!isCompact && block}
</div>
<div class="footer">
Footer content
{isCompact && block}
</div>
</div>
);
With Vue by default you have no other option but to duplicate block:
<template>
<div class="container">
<div class="header">
Header content
<div v-if="!isCompact"
class="block"
@click="handleClick">
Block that should be reused
</div>
</div>
<div class="footer">
Footer content
<div v-if="isCompact"
class="block"
@click="handleClick">
Block that should be reused
</div>
</div>
</div>
</template>
What is VueUse
VueUse is a utility library aimed at solving common use-cases packed with useful composables and other helpers similar to react-use in the React ecosystem.
Reusing parts of the template
VueUse provides createReusableTemplate function that can help to DRY your templates.
As it’s a separate package and not a part of Vue framework API it needs to be installed with a command like:
npm i @vueuse/core
Check out the installation guide for instructions for your package manager of choice or meta framework (if you’re using Nuxt).
To reuse markup you need to first call createReusableTemplate()
in <script>
section:
<script setup lang="ts">
import { createReusableTemplate } from '@vueuse/core'
const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
</script>
It returns two component constructors: DefineTemplate
to wrap the template part you want to reuse and ReuseTemplate
to render the part in place. You can then use them in <template>
like so:
<template>
<DefineTemplate>
<div
class="block"
@click="handleClick">
Block that should be reused
</div>
</DefineTemplate>
<div class="container">
<div class="header">
Header content
<ReuseTemplate v-if="!isCompact" />
</div>
<div class="footer">
Footer content
<ReuseTemplate v-if="isCompact" />
</div>
</div>
</template>
<DefineTemplate>
must be used before <ReuseTemplate>
.
You can’t however use return of the one createReusableTemplate()
call to reuse different blocks simultaneously.
<template>
<DefineTemplate>
<div>Reused block A</div>
</DefineTemplate>
<ReuseTemplate />
<!-- registers another template instead -->
<DefineTemplate>
<div>Reused block B</div>
</DefineTemplate>
<!-- will now render new template -->
<ReuseTemplate />
</template>
So instead you would need to call createReusableTemplate() for each individual reused piece of template you want to use simultaneously in the markup:
<script setup>
import { createReusableTemplate } from '@vueuse/core'
const [DefineBlockA, ReuseBlockA] = createReusableTemplate()
const [DefineBlockB, ReuseBlockB] = createReusableTemplate()
</script>
<template>
<DefineBlockA>
<div>Reused block A</div>
</DefineBlockA>
<DefineBlockB>
<div>Reused block B</div>
</DefineBlockB>
<div class='text-comment'><!-- the two can exist simultaneously --></div>
<ReuseBlockA />
<ReuseBlockB />
</template>
Analogue to React render functions
Having ability to reuse the same a template part is good but with JSX in React one can also define a render function to also dynamically pass different props to the reused piece:
const renderBlock = (className: string) => (
<div
class={`block ${className}`}
onClick="handleClick">
Block that should be reused and customized.
</div>
);
return (
<div class="container">
<div class="header">
Header content
{!isCompact && renderBlock("full")}
</div>
<div class="footer">
Footer content
{isCompact && renderBlock("compact")}
</div>
</div>
);
Vue provides Scoped Slots as the general alternative to React JSX render functions and createReusableTemplate supports this framework feature to pass data to the reused template. You can use the same v-slot like you would normally would to pass data from child component back to parent to reverse the control flow.
<script setup lang="ts">
import { createReusableTemplate } from '@vueuse/core'
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{ className: string }>()
</script>
createReusableTemplate
accepts a generic type argument for typing the data passed to the template similar to specifying render function signature.
You can then bind props to ReuseTemplate
and it will be accessible in the reusable part inside DefineTemplate
via v-slot
:
<template>
<DefineTemplate v-slot="{ className }">
<div
:class="['block', className]"
@click="handleClick">
Block that should be reused and customized.
</div>
</DefineTemplate>
<div class="container">
<div class="header">
Header content
<ReuseTemplate v-if="!isCompact" className="full" />
</div>
<div class="footer">
Footer content
<ReuseTemplate v-if="isCompact" className="compact" />
</div>
</div>
</template>
Render functions in JSX can also accept JSX as their arguments to be rendered somewhere inside:
const renderBlock = (className: string, content: ReactNode) => (
<div
class={`block ${className}`}
onClick="handleClick">
Block that should be reused with custom content.
Custom content: {content}
</div>
);
return (
<div class="container">
<div class="header">
Header content
{!isCompact && renderBlock(
"full",
<div>Full content</div>
)}
</div>
<div class="footer">
Footer content
{isCompact && renderBlock(
"compact",
<div>Compact content</div>
)}
</div>
</div>
);
But no worries, createReusableTemplate got you covered with this one as well as ReuseTemplate can also be passed slots that can be accessed via $slots on v-slot and rendered inside DefineTemplate with Dynamic Components syntax:
<template>
<DefineTemplate v-slot="{ className, $slots }">
<div
:class="['block', className]"
@click="handleClick">
Block that should be reused with custom content.
Custom content:
<!-- To render the slot -->
<component :is="$slots.default" />
</div>
</DefineTemplate>
<div class="container">
<div class="header">
Header content
<ReuseTemplate v-if="!isCompact" className="full">
<div>Full content</div>
</ReuseTemplate>
</div>
<div class="footer">
Footer content
<ReuseTemplate v-if="isCompact" className="compact">
<div>Compact content</div>
</ReuseTemplate>
</div>
</div>
</template>
That’s it!
If you’re a React dev struggling to embrace Vue because you feel down having to write templates consider giving it another shot with this addon.
Alternatively, if this still isn’t enough to make templates plausible for you can learn how to use JSX in Vue to make your experience with Vue more close to home.