Dynamic component rendering in Vue 3

Dynamic component rendering in Vue 3

Thu Jan 19 2023665 words

Why would you need to import components dynamically in Vue 3?

If your are building modular pages or working with polymorphic data structures you may find yourself having to create a list of Vue components dynamically. Take the following example of a modular page with 3 sections:

  • BannerComponent
  • MarkdownComponent
  • PhotoComponent

The above components could just be hard coded, however if the page is CMS driven or you are using a list of static data you would need to create the 3 child components dynamically.

Vue 3 dynamic async components

You can dynamically set the import path on an async component. This means you could use static data or CMS driven data to define the type of component to render.

<script setup lang="ts">
const componentPath = '@/components/BannerComponent'

const is = computed(
    () => defineAsyncComponent(() => import(`${componentPath}.vue`))
)
</script>

<template>
    <component :is="is" />
</template>

Another approach is to import the components, assign them to an object, then select them by key. However using async components means you can take advantage of lazy loading.

<script setup lang="ts">
import BannerComponent from '../Components/BannerComponent.vue'
import MarkdownComponent from '../Components/MarkdownComponent.vue'

const morphs = {
    BannerComponent,
    MarkdownComponent,
}
const components = [
    'BannerComponent',
    'MarkdownComponent',
];
</script>

<template>
    <component
        v-for="(component, i) in components"
        :key="i"
        :is="morphs[component]"
    />
</template>

Creating modular page components in Vue 3 and TypeScript

The following is a strategy for creating modular pages consisting of component data structures that can be driven by CMS or static data.

You can express a component as a TypeScript interface.

interface PageComponent {
    is: string,
    props: Record<string,unknown>
}

To make this predictable and type safe component types can be added

type ComponentType = 'BannerComponent' | 'MarkdownComponent' | 'PhotoComponent'

interface PageComponent {
    is: ComponentType,
    props: Record<string,unknown>
}

Prop types can also be added. These props types should be the exact props passed into each component.

interface BannerComponentProps {
    title: string,
}
interface MarkdownComponentProps {
    markdown: string,
}
interface PhotoComponentProps {
    image: string,
}
type ComponentProps = BannerComponentProps | MarkdownComponentProps | PhotoComponentProps

interface PageComponent {
    is: ComponentType,
    props: ComponentProps
}

Using the above types its easy to construct a simple list of data with which can be used to render a group of components.

const components: Array<PageComponent> = [
    {
        is: '@/components/BannerComponent',
        props: {
            title: 'banner title'
        } as BannerComponentProps
    },
    {
        is: '@/components/MarkdownComponent',
        props: {
            markdown: 'markdown text'
        } as MarkdownComponentProps
    },
    {
        is: '@/components/PhotoComponent',
        props: {
            image: 'image URL'
        } as  PhotoComponentProps
    },
]

Two components are needed to render the list of components

  • PageComponentList.vue
  • PageComponentListItem.vue

PageComponentList.vue receives the array of PageComponent data as a prop, loops through it, and passes each PageComponent object to a PageComponentListItem component

<script setup lang="ts">
defineProps<{
    components: Array<PageComponent>
}>()
</script>

<template>
    <PageComponentListItem
        v-for="(component, i) in components"
        :key="i"
        :component="component"
    />
</template>

PageComponentListItem.vue receives the PageComponent object then dynamically creates the component and binds the props from the object data.

<script setup lang="ts">
import { computed, defineAsyncComponent } from 'vue'

const props = defineProps<{
    component: PageComponent
}>()

const is = computed(() =>
    defineAsyncComponent(() => import(`${props.component.is}.vue`))
)
</script>

<template>
    <component :is="is" v-bind="component.props" />
</template>

The above is a simple but powerful approach to creating pages dynamically and works especially well with CMS driven data.

Featured Articles