When I stumble across something that makes my code cleaner, I'm all in. Dynamic Vue components have come to the rescue in several projects I've worked on, and I'm here to show you how to use them to cleanly render component variations on similar sets of data.
Let's dive right in and make it clear what I'm talking about. Imagine you have a list of notifications coming through from an API, with data that looks something like this:
[
{
id: 1,
type: 'like',
data: {
user: {
id: 128,
username: "Mabel"
},
article: {
id: 11,
title: "The Power of Dynamic Vue Components"
}
}
},
{
id: 2,
type: 'comment',
data: {
user: {
id: 128,
username: "Billy"
},
comment: {
id: 1567,
body: "Much learning. Such informative."
}
}
}
]
We have two notifications, both with their own related (but different) data:
We also have a type which tells us which kind of notification it is. In an ideal world, if all of these notifications had the same type, we'd iterate through them and just output the data. But they're not.
A bunch of IF statements will make this work this while looking ugly. Let's do it anyway!
<template>
<div>
<div
v-for="notification in notifications"
:key="notification.id"
>
<template v-if="notification.type === 'like'">
{{ notification.data.username }} liked your article "{{ notification.data.article.title }}"!
</template>
<template v-else-if="notification.type === 'comment'">
{{ notification.data.username }} commented on your article.
<div>
{{ notification.data.comment.body }}
</div>
</template>
</div>
</div>
</template>
And that's just with two notification types, zero styling, and simple output of the example data we have. I dread to think how this would look as we rolled out more notification types 🙀. Let's move on quickly.
One option to clean up this muck would be to break these notification types into components and pass data down as a prop. This goes halfway to our final solution.
Here are the two notification types, broken up into components.
<template>
<div>
{{ data.user.username }} liked your article "{{ data.article.title }}"!
</div>
</template>
<script>
export default {
name: 'AppNotificationVariationLike',
props: {
data: {
required: true,
type: Object
}
}
}
</script>
<template>
<div>
{{ data.user.username }} commented on your article.
<div>
{{ data.comment.body }}
</div>
</div>
</template>
<script>
export default {
name: 'AppNotificationVariationComment',
props: {
data: {
required: true,
type: Object
}
}
}
</script>
Notice the names I've chosen for these components. I always prefix component names with App
followed by what the thing is (in this case a NotificationVariation
), and finally, I'm adding the type (Like
and Comment
) at the end.
Now our decision component looks a lot better.
<template>
<div>
<div
v-for="notification in notifications"
:key="notification.id"
>
<template v-if="notification.type === 'like'">
<AppNotificationVariationLike :data="notification.data" />
</template>
<template v-else-if="notification.type === 'comment'">
<AppNotificationVariationComment :data="notification.data" />
</template>
</div>
</div>
</template>
You could even reduce this further and get rid of those templates, which we don't need anymore.
<template>
<div>
<div
v-for="notification in notifications"
:key="notification.id"
>
<AppNotificationVariationLike
:data="notification.data"
v-if="notification.type === 'like'"
/>
<AppNotificationVariationComment
:data="notification.data"
v-else-if="notification.type === 'comment'"
/>
</div>
</div>
</template>
I'm still not happy though. I want clean code, and I want it now.
You should have a pretty good idea of what we're trying to do now. You might have even noticed a pattern, particularly in the naming of the components.
Luckily for us, Vue has a special component
element that we can use to render a registered or imported component by passing the name to it. It looks like this.
<component :is="AnyComponentYouFancy" />
Putting two and two together, we can render each of our component types based on the type
from our API.
<template>
<div>
<div
v-for="notification in notifications"
:key="notification.id"
>
<component
:is="`AppNotificationVariation${notification.type}`"
:data="notification.data"
/>
</div>
</div>
</template>
To explain. The :is
attribute value is changing based on the type
in each iteration, rendering a different component for each notification, while also sending down the data
prop with everything we need. Win!
Stop right there though. Right now, our type
from the API is lowercase, and we're using Pascal Case for our component names (meaning the value for the :is
attribute is now something like AppNotificationVariationlike
, with a lowercase L). We can fix this by either:
All options are fine, but the last two make more sense.
If we rename our components to match up with the lowercase API type, AppNotificationVariationComment
would become AppNotificationVariation-comment
. This means we could directly use the type
from the API to match up with the renamed component. I still don't like this as much as the last option.
So let's go for the last option.
<template>
<div>
<div
v-for="notification in notifications"
:key="notification.id"
>
<component
:is="`AppNotificationVariation-${notification.type}`"
:data="notification.data"
/>
</div>
</div>
</template>
<script>
import AppNotificationVariationLike from '@/notifications/variations/AppNotificationVariationLike'
import AppNotificationVariationComment from '@/notifications/variations/AppNotificationVariationComment'
export default {
components: {
'AppNotificationVariation-like': AppNotificationVariationLike,
'AppNotificationVariation-comment': AppNotificationVariationComment,
}
}
</script>
You could shorten this even further to be honest.
<template>
<div>
<div
v-for="notification in notifications"
:key="notification.id"
>
<component
:is="notification.type"
:data="notification.data"
/>
</div>
</div>
</template>
<script>
import AppNotificationVariationLike from '@/notifications/variations/AppNotificationVariationLike'
import AppNotificationVariationComment from '@/notifications/variations/AppNotificationVariationComment'
export default {
components: {
'like': AppNotificationVariationLike,
'comment': AppNotificationVariationComment,
}
}
</script>
Or somewhere in-between. Whatever feels right for you.
Finally, we have component file names as we want them, only changing the name to suit our data when we register them, and then we're using them in our component
element to avoid that disgusting example from earlier in the article.
When you create a new notification type and output it from your API, you now just create a component for that type, register it based on the type and you're done. No more IF ELSE statements and your notification components are tucked away neatly in place, making finding, updating and testing easier.
I've used the example of notifications here, but dynamic Vue component can be used for anything. Next time you find yourself dealing with a similar scenario, hopefully dynamic components can help you out.