Pass props to dynamic Vue components
Tags: vue, javascript, webdev
Published: 2020-09-04 17:23:56

---
title: Pass props to dynamic Vue components
published: true
date: 2020-09-04 17:20:00 UTC
tags: vue, javascript, webdev
canonical_url: https://jwkicklighter.com/posts/pass-props-to-dynamic-vue-components/
---

*This article is also posted on my [blog](https://jwkicklighter.com/posts/pass-props-to-dynamic-vue-components/).*

While brainstorming some new Vue components (that sounds [familiar](https://jwkicklighter.com/posts/pass-all-props-to-children-in-vue/)), I thought it would be nice to loop over a list of items and dynamically render the correct component for each one. The problem is that the components being rendered do not take the same props.

_Disclaimer: this article is a bit more complicated than my last one. I assume that you understand the basics of Vue components and that you have some knowledge of modern JS patterns such as `Array.map`._

In Vue, it's very common to loop over a list of items and render a component for each item. This is usually done by specifying the component that will be rendered and adding a `v-for` to its tag in the template.

```
<!-- Contacts.vue -->
<template>
  <contact-card
    v-for="person in people"
    :key="person.id"
    :contact="person" />
</template>

<script>
  import ContactCard from 'ContactCard.vue'

  export default {
    components: {
      ContactCard,
    },
    props: {
      people: {
        type: Array,
        default: () => [],
      }
    }
  }
</script>
```

This is a very straightforward pattern seen often in Vue apps. In the example above, we have a component `ContactCard` that is meant to display a person's contact information. Let's take a closer look at `ContactCard.vue` to get a feel for what's going on.

```
<!-- ContactCard.vue -->
<template>
  <article>
    <h1>{{ contact.name }}</h1>
    <ul>
      <li>Phone: {{ contact.phone }}</li>
      <li>Email: {{ contact.email }}</li>
    </ul>
  </article>
</template>
...
```

So as you can see, we have a very basic component for displaying details of a contact. The root level is an article tag (yay HTML5) with an h1 tag containing the person's name and an unordered list of some contact information. Nothing crazy, but there is a required prop called `contact`.

# Dynamic Components

As we build more of the app, we get the idea to add a timeline that shows the order that data was added to the system, which includes creating contacts. Because we already have such a robust component for displaying contact information, we decided to re-use it in this timeline view. Let's take a look at a new component for that.

```
<!-- Timeline.vue -->
<template>
  <contact-card
    v-for="item in history"
    :key="item.id"
    :contact="item" />
</template>

<script>
  import { historyArray } from 'Data'
  import ContactCard from 'ContactCard.vue'

  export default {
    components: {
      ContactCard,
    },
    computed: {
      history () {
        return historyArray
      },
    },
  }
</script>
```

At first glance, the `Timeline` component probably looks great. But let's look at the structure of `historyArray`:

```
[
  {
    id: 'contact-1',
    isContactItem: true,
    ...
  },
  {
    id: 'event-1',
    isSystemEventItem: true,
    ...
  },
  {
    id: 'contact-2',
    isContactItem: true,
    ...
  },
]
```

A closer look reveals that there are more than just "contacts" in this historical data. Indeed, we have contacts **and** system events to display. Luckily, someone has already created a `SystemEvent` component so we don't have to. Much like our `ContactCard` this component has a single required prop (`event` in this case) and displays relevant information about that prop. So let's change the `Timeline` component to dynamically decide which component to use for each item in the list.

```
<!-- Timeline.vue -->
<template>
  <component
    v-for="item in history"
    :key="item.id"
    :is="item.component"
    :contact="item" />
</template>

<script>
  import { historyArray } from 'Data'
  import ContactCard from 'ContactCard.vue'
  import SystemEvent from 'SystemEvent.vue'

  export default {
    components: {
      ContactCard,
      SystemEvent,
    },
    computed: {
      history () {
        return historyArray.map(historyItem => {
          if (historyItem.isContactItem) {
            // Return a new object that contains ALL keys
            // from `historyItem` and adds a `component` key
            return {
              ...historyItem,
              component: ContactCard,
            }
          } else if (historyItem.isSystemEventItem) {
            return {
              ...historyItem,
              component: SystemEvent,
            }
          }
        })
      },
    },
  }
</script>
```

You can see that instead of specifying `contact-card` in the template, we are now using a special tag called `component`. Along with this, there is a new `is` prop being passed in. Inside of the `history` computed value, we are checking each item to see if it is a contact or system event item (using some special helper functions that we assume exist), and we add the key `component` that contains the relevant component to render for that particular item. In the loop within the template, the `is` prop gets bound to that `component` key. The end result is that contact items cause a `ContactCard` component to be rendered and system event items cause a `SystemEvent` component to be rendered.

_Note: you can read up on the `component` tag in the [Vue docs](https://vuejs.org/v2/guide/components.html#Dynamic-Components)._

If you're paying close attention, you may notice a slight problem: the `SystemEvent` component takes a prop called `event`, but the template is currently passing it a prop called `contact`. How can we get around that? Well one option is to pass both `contact` and `event` props to every component. This technically won't cause a problem, but it feels a bit messy. Regardless, let's see what that might look like.

```
<!-- Timeline.vue -->
<template>
  <component
    v-for="item in history"
    :key="item.id"
    :is="item.component"
    :contact="item"
    :event="item" />
</template>

...
```

Now every component in the list will be passed `contact` and `event`. They are both being passed the same variable, so the `ContactCard` component will see the `contact` prop and behave correctly, and the `SystemEvent` component will see the `event` prop and behave correctly. This will work fine, but as you can imagine could quickly get out of hand if we have components with numerous props needed. There has to be a better way...

# Dynamic Props

Wait a minute! If we are dynamically declaring what _component_ is going to be rendered, can't we dynamically declare what _props_ that component should receive? If you read my [last Vue post](https://jwkicklighter.com/posts/pass-all-props-to-children-in-vue/), then you already know that `v-bind` allows you to bind an entire set of props in one go. So let's see if we can apply that here.

_Note: You can read more about passing an object's properties with `v-bind` in the [Vue docs](https://vuejs.org/v2/guide/components-props.html#Passing-the-Properties-of-an-Object)._

```
<!-- Timeline.vue -->
<template>
  <component
    v-for="item in history"
    :key="item.id"
    :is="item.component"
    v-bind="item.props" />
</template>

<script>
  import { historyArray } from 'Data'
  import ContactCard from 'ContactCard.vue'
  import SystemEvent from 'SystemEvent.vue'

  export default {
    components: {
      ContactCard,
      SystemEvent,
    },
    computed: {
      history () {
        return historyArray.map(historyItem => {
          if (historyItem.isContactItem) {
            // Return a new object that contains a `component`
            // key, an `id` key, and a `props` object
            return {
              id: historyItem.id,
              component: ContactCard,
              props: {
                contact: historyItem,
              },
            }
          } else if (historyItem.isSystemEventItem) {
            return {
              id: historyItem.id,
              component: ContactCard,
              props: {
                event: historyItem,
              },
            }
          }
        })
      },
    },
  }
</script>
```

Alright, I know the function for our `history` computed value is starting to get crazy, but it's really not doing much. If it's hard to follow, here is an example of what the resulting data structure would look like:

```
[
  {
    id: 'contact-1',
    component: ContactCard,
    props: {
      contact: {...}
    }
  },
  {
    id: 'event-1',
    component: SystemEvent,
    props: {
      event: {...}
    }
  },
  {
    id: 'contact-2',
    component: ContactCard,
    props: {
      contact: {...}
    }
  }
]
```

Take another look at the template now that you have an idea of how `history` is structured. Notice that the `contact` and `event` props were removed, and we just have a single `v-bind` instead. The value we give `v-bind` is `item.props`, which according to the snippet above, will contain the prop that is appropriate for each component. This is much cleaner than our previous approach, and keeps the template easy to read. If the components differed more, the `history` logic could easily be broken into multiple functions.

# Summary

Sometimes there is a need to dynamically choose the component to display when looping over a list of items. This is very well supported by VueJS and made easy using the `component` tag. As complexity grows and components begin to be re-used throughout an application, these components may have prop interfaces that aren't really compatible with each other. Dynamically binding props to the component, just like dynamically declaring the component to be used, helps keep the template clean and readable.