Hi guys, in today’s post I want to share an interesting scenario about using dynamic components with you.
Imagine this situation where you have a search feature in your application. A pretty common feature to have, right? There’s a search field, maybe some more advanced filters to complement it, you grab the results from the backend and need to display what was found.
It’s reasonable to think of writing something like the code below:
<div
v-for="(item, index) of items"
:key="index"
>
<!-- Results are displayed here... -->
</div>
Now, imagine that this search brings all kinds of results, where the backend endpoint returns multiple entities, and each entity is displayed differently on the page. Something like this:
{
"items": [
{
"type": "client",
"data": { "id": 1, "name": "John Doe", "phoneNumber": "+5512345678" }
},
{
"type": "product",
"data": { "id": 98, "description": "Product description", "imageURL": "https://someimage.png", "value": 10.99 }
},
{
"type": "service",
"data": { "id": 86, "description": "Service description", "notes": "Notes about this service", "value": 100.53 }
}
]
}
Looking at the payload above we can see three different categories of information. To display the items on the page we have the following component:
<div
v-for="(item, index) of items"
:key="index"
>
<div v-if="item.type == 'service'">
<!-- service information goes here... -->
</div>
<div v-if="item.type == 'product'">
<!-- product information goes here... -->
</div>
<div v-if="item.type == 'client'">
<!-- client information goes here... -->
</div>
</div>
Break it into small pieces
The code shown previously will work, but, we can (and should) break it down into smaller pieces.
Some lines of code were omitted from the last code snippet. Keep in mind that you’ll have more elements for each category. It’s too easy to be caught in a mess thinking like that… So, we can improve it by organizing each category into a specific component. By doing this our code will look more like this:
<!-- SearchResult.vue -->
<template>
<div
v-for="(item, index) of items"
:key="index"
>
<DisplayService :data="item.data" v-if="item.type == 'service'" />
<DisplayProduct :data="item.data" v-if="item.type == 'product'" />
<DisplayClient :data="item.data" v-if="item.type == 'client'" />
</div>
</template>
<script>
import DisplayService from 'DisplayService.vue';
import DisplayProduct from 'DisplayProduct.vue';
import DisplayClient from 'DisplayClient.vue';
export default {
name: 'SearchResult',
components: {DisplayService, DisplayProduct, DisplayClient},
setup() {}
}
</script>
Each component has its logic and its template to display the information. They just receive the data
prop. Let’s look a little bit at the structure of the DisplayClient.vue
component.
<template>
<!-- the template goes here -->
</template>
<script>
export default {
name: 'DisplayClient',
props: {
data: {type: Object, required: true, default: null}
},
setup() {}
};
</script>
The above example shows the structure of the DisplayClient
. This is structure is quite similar to the DisplayService
and DisplayProduct
.
Let’s look for improvement points
Our last SearchResult.vue
snippet is a little better than the first one presented. Although it’s better organized it isn’t very different looking from the SerchResult.vue
perspective.
We have main concerns to pay attention to in our current code:
- we’re still using
v-if
to display each piece of information; - we’re importing components that might not be used.
The second one is more about dynamic loading. To clarify, suppose that the endpoint returns all the items as being services
. It means that we only need to display the DisplayService
component. However, we’re importing two other components, and in this possible scenario, they won’t be used. With dynamic loading, we can avoid this and import only the components we’re going to use.
Let’s make these improvements!
Implementing dynamic components
At first, let’s look at a more complete version of SearchResult.vue
after implementing dynamic components.
<!-- SearchResult.vue -->
<template>
<component
v-for="(item, index) of items"
:key="index"
:is="mapTypeComponents[item.type]"
v-bind="item"
/>
</template>
<script>
import DisplayService from 'DisplayService.vue';
import DisplayProduct from 'DisplayProduct.vue';
import DisplayClient from 'DisplayClient.vue';
import { ref } from 'vue';
export default {
name: 'SearchResult',
components: {DisplayService, DisplayProduct, DisplayClient},
setup() {
const items = ref([]);
const mapTypeComponents = {
product: DisplayProduct,
service: DisplayService,
client: DisplayClient
};
const doSearch = async () => {
items.value = await getItems();
};
return {
mapTypeComponents,
doSearch,
items
};
}
}
</script>
Let’s talk about each change made in our code.
The usage of
component
This
component
tag means that this is a dynamic component where we can define the component that will be loaded by assigning theis
prop.To make it happen we have the object mapTypeComponents which maps the type to the corresponding component.
The
v-for
The
v-for
is now in the component not in a parentdiv
since we don’t need to display the component based on av-if
.Props passing
The
v-bind
passes theitem
attribute as props to the component, thus each component can receivetype
anddata
as props. But we programmed our components to get just thedata
which is enough for what we need.
Our code looks better. It is more readable as well as more organized than the previous versions.
Loading components dynamically
Now that we have our dynamic components coded and ready to go, let’s save a few resources on our page by loading only what is needed to render the search results.
The concept behind this is the Async Components which are described in the VueJS documentation.
In large applications, we may need to divide the app into smaller chunks and only load a component from the server when it’s needed. To make that easier, Vue allows you to define your component as a factory function that asynchronously resolves your component definition.
To get it done we need to adjust our mapTypeComponents
by importing components using the composable defineAsyncComponent
.
As a reminder, this approach is going to work if you’re using Composition API and Vue 3. Check out the link in References section and get to know how to implement using Options API and/or Vue 2.
Using the defineAsyncComponent
our code will look like this:
<template>
<component
v-for="(item, index) of items"
:key="index"
:is="mapTypeComponents[item.type]"
v-bind="item"
/>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue';
export default {
name: 'SearchResult',
setup() {
const items = ref([]);
const mapTypeComponents = {
product: defineAsyncComponent(() => import('DisplayProduct.vue')),
service: defineAsyncComponent(() => import('DisplayService.vue')),
client: defineAsyncComponent(() => import('DisplayClient.vue'))
};
const doSearch = async () => {
items.value = await getItems();
};
return {
mapTypeComponents,
doSearch,
items
};
}
}
</script>
We removed the components importing right after the script
tag, and we changed the object mapTypeComponents
replacing the component itself with the defineAsyncComponent
with a function returning the import inside of it.
Summary
In this article, we learned how to use some powerful VueJS concepts. Concepts that, in this case, made our solution more readable, elegant, and performative. All that is in a context that you might face within your journey as a VueJS developer.
Let me know if you have any questions and If you liked this content consider sharing it with your friends.
That’s all. See ya!
References
We want to work with you. Check out our "What We Do" section!