Vue.js 3 Dynamic & Built-in Components Tutorial

In this Vue tutorial we learn how to switch between components without routing with dynamic components.

We cover how to keep data alive between switches, the meta component and required props.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need an app generated by the Vue CLI as well as the following extra components.

  • src/components/StepA.vue
  • src/components/StepB.vue

The project should look similar to the following.

Example: project
project-folder/
├── src/
|   ├── components/
|   |   ├── StepA.vue
|   |   └── StepB.vue
|   └── App.vue

We want the new components to be nested inside the root app component.

Example: src/App.vue
<template>
  <step-a />
  <step-B />
</template>

<script>
import StepA from "./components/StepA";
import StepB from "./components/StepB";

export default {
  components: {
    StepA,
    StepB
  }
}
</script>

The two new components can each have a heading with some identifying text.

Example: src/components/StepA.vue
<template>
  <h2>Step A Component</h2>
</template>
Example: src/components/StepB.vue
<template>
  <h2>Step B Component</h2>
</template>

What are Dynamic Components?

Vue uses what’s known as routes to switch between components. We can think of it as navigating between pages in a regular website.

For example, clicking on an About Us link in a Vue application will route to example.com/about-us . To the user it looks like they navigate to a new page when in reality Vue only loaded the AboutUs component.

Dynamic components allow us to switch between two or more components without using routes, retaining the state of data on any of them if required.

As an example, let’s consider a multi-step form where each step is a different component. The data from the previous steps need to persist in the event the user needs to go back.

How to create a dynamic component

To create a dynamic component, we use the component element and bind the is prop to it to specify which component we want to render.

note The component and slot elements aren’t true components, they are component-like features of Vue’s template syntax. We do not have to import them like we do with regular components.

Syntax: dynamic component
<component :is="component_name" />
// or
<component :is="component_name"></component>

If we consider the multi-step form example, the component we want to render on each step will be specified in the is prop.

To demonstrate, let’s create an example without dynamic components first. We’ll have two buttons in our root App component that switch between the step components based on the button that’s clicked.

Example: src/App.vue
<template>
  <p>
    <button @click="activeStep = 'StepA'">Step A</button>
    <button @click="activeStep = 'StepB'">Step B</button>
  </p>

  <step-a v-if="activeStep === 'StepA'" />
  <step-b v-if="activeStep === 'StepB'" />
</template>

<script>
import StepA from "./components/StepA.vue";
import StepB from "./components/StepB.vue";

export default {
  components: { StepA, StepB },
  data() {
    return { activeStep: 'StepA' }
  }
}
</script>

If we run the example and click on a button, it will load that component.

While this is a perfectly valid approach, what if there are many steps to the form. It’s not uncommon for multi-step forms to have six or eight steps, which can result in a lot of code.

The second problem is that data from one component doesn’t persist if we load the second component, which is necessary in a multi-step form.

We’ll look at how to keep data alive between components in a little bit. First, let’s replace our two step components with a single component element and bind the is prop to it.

Because activeStep contains the component name, we can use it in the is prop to specify the component we want to render.

Example:
<template>
  <p>
    <button @click="activeStep = 'StepA'">Step A</button>
    <button @click="activeStep = 'StepB'">Step B</button>
  </p>

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

<script>
import StepA from "./components/StepA.vue";
import StepB from "./components/StepB.vue";

export default {
  components: { StepA, StepB },
  data() {
    return { activeStep: 'StepA' }
  }
}
</script>

If we save the file and take a look in the browser, it works exactly the same as it did before.

Vue loads the component based on the is prop’s value. The value coming from activeStep will be either “StepA” or “StepB”, so it will render the StepA or StepB component.

There is no need for us to invoke the components manually and create a v-if condition for each of them. Our components are now rendered dynamically.

Rendered component casing

We mentioned that Vue will render the StepA or StepB components. In Vue it’s legal for a component instance to use PascalCase, which is what happens in our case because we use PascalCase in the activeStep data property.

So technically, Vue will render them as follows.

Example:
<template>
  <StepA />
  <StepB />
</template>

But, conventionally we should use kebab-case.

If we want to follow convention, we should change the value in activeStep and the button click events to use kebab case.

Example: src/App.vue
<template>
  <p>
    <button @click="activeStep = 'step-a'">Step A</button>
    <button @click="activeStep = 'step-b'">Step B</button>
  </p>

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

<script>
import StepA from "./components/StepA.vue";
import StepB from "./components/StepB.vue";

export default {
  components: { StepA, StepB },
  data() {
    return { activeStep: 'step-a' }
  }
}
</script>

Now, Vue will render them as follows.

Example:
<template>
  <step-a />
  <step-b />
</template>

Everything will still work as expected but now our code is consistent with convention.

How to keep data alive between components

When we use dynamic components, we may want data to persist when switching between them.

For example, our multi-step form should allow users to move back and forth between different steps without losing the data they already entered.

To demonstrate this default behavior, let’s modify the StepB component to have a text input.

Example: src/components/StepB.vue
<template>
  <h2>Step B Component</h2>
  <p>Name: {{ name }}</p>
  <input type="text" v-model="name">
</template>

<script>
export default {
  data() {
    return { name: '' }
  }
}
</script>

If we click on the “Step B” button in the browser and fill in a name in the input field, the name is saved to the data property and displayed above the field. But if we click on “Step A” and then back on “Step B”, the name we entered is lost.

Vue can cache dynamic components, allowing their data to persist. To do that, we wrap our component instance with the open-and-close keep-alive component.

Syntax: keep-alive
<keep-alive>
  <component :is="component_name" />
</keep-alive>

To demonstrate, let’s wrap the dynamic component in our example with the keep-alive tags.

Example: src/App.vue
<template>
  <p>
    <button @click="activeStep = 'step-a'">Step A</button>
    <button @click="activeStep = 'step-b'">Step B</button>
  </p>

  <keep-alive>
    <component :is="activeStep" />
  </keep-alive>
</template>

<script>
import StepA from "./components/StepA.vue";
import StepB from "./components/StepB.vue";

export default {
  components: { StepA, StepB },
  data() {
    return { activeStep: 'step-a' }
  }
}
</script>

This time, the name will persist if we switch between the components.