The Power Of Dynamic Vue Components

March 22nd, 2020

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:

  • Someone liked our article (a user liked our article)
  • Someone commented on our article (a user left a comment)

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.

Dirty solution

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.

Breaking up notifications into their own components

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.

Enter Dynamic Vue Components

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:

  • Changing the type in the API to Pascal Case
  • Creating a function to transform strings to Pascal Case (no, thanks)
  • Changing the name of our registered component
  • Import the components as they are, but dynamically assign them a different name

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.

Your future self thanks you

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.

Author
Alex Garrett-Smith

Comments

No coments, yet. Be the first to leave a comment.