VueT2 Dark_1200x303

In this article we will explore Vue's <component>, <keep-alive> and how to create dynamically loaded components.

A time comes in any Vue developer’s life when you find yourself wishing that a particular piece of your template could be dynamic and switch between one or many components depending on certain conditions that are calculated elsewhere.

Take for example a website that renders some specific content only when a user is registered and logged in, but it also has a third version that is displayed if this particular user has a paid subscription.

One way to approach the problem could be chaining v-ifs like so:

<template>
  <div>
		 <Guest v-if="!user" />
		 <User v-if="user && !user.subscription" />
		 <Member v-if="user && user.subscription" />
  </div>
</template>

This will certainly work but even with good component composition it can quickly start to become a problem. If your logic becomes more complex or you have to add different components over time, your template will become crowded and difficult to maintain.

Another common scenario is having an API endpoint telling you what sort of components it wants the frontend to render on the screen—a schema or manifest of sorts. This particular case can make for some very powerful applications, but we have to delve into how to create dynamic components.

The :is Directive and <component>

Vue gives us a special component and a directive to approach this type of problem, the <component> component. This special component behaves like a placeholder in your template. It will render any type of element inside of it—from custom components like Guest to basic HTML elements like <li>.

In order for us to use <component>, we have to pass to it an attribute named is. This directive expects a value, that is either a String or an Object, a registered component, or a component config object. Let’s look first into how to do this with strings.

Let’s rethink our previous example with the use of <component>. Our template is going to get simplified a lot, into the following:

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

At this point you can begin to see how powerful <component> can be: Our template is cleaner, and soon we will add a computed property to encapsulate our logic.

We are going to create a computed property, userComponent, that will let <component> know which component it should render. To do this, we are going to move our logic to this computed property, and return a string with the name of the component that should be rendered.

Important: Be aware that when using the string approach, your components need to be registered into your application, either as global components, or imported into your component and registered under the components option that will render them.

Another way of doing this, as you will see below, is returning the imported component itself in the computed property.

If you forget to do this, Vue will issue a warning. “Unknown component : did you register the component correctly?”

Let’s now add the computed property, and the necessary imports.

<script>
import Guest from './components/Guest';
import User from './components/User';
import Member from './components/Member';

export default {
  data() {
    return {
      user: null
    } 
  },
  computed: {
    userComponent() {
      if (!this.user) return Guest;
      if (!this.user.subscription) return User;
      
      return Member;
    }
  }
};
</script>

On the top we are importing all three components as usual. Notice that we’re also creating a local data() state for the user object for demo purposes.

Inside the userComponent computed prop, we are transferring our logic that was previously on the template. You can already appreciate that this is both way more readable, and most importantly a lot more flexible. You can use Axios, or Vuex—or even load a dynamic schema from a file on your application here to define which component is supposed to be rendered. The possibilities are endless!

Dynamic Loading

Ok so right now as it is, we already have added value on how our component is getting put onto the user’s screen, but I can still see a problem with it. The components need to still be added manually into our application, and they are ALL loaded even if the user navigating our site never gets to see them.

Let’s take some steps to make these components dynamically load as they are requested by the browser, this will make our app bundle size smaller, as webpack will place them in external chunks.

For this approach to work, we will modify our computed property to return a function, which in return will import a component dynamically.

<script>
const Guest = () => import("./components/Guest")
const User = () => import("./components/User")
const Member = () => import("./components/Member")

export default {
  data() {
    return {
      user: null
    };
  },
  computed: {
    userComponent() {
      if (!this.user) return Guest;
      if (this.user && !this.user.subscription)
        return User;
      if (this.user && this.user.subscription)
        return Member;
    }
  }
};
</script>

Notice first that all of our import statements on the top are now gone. This is no longer necessary, as the components will load dynamically and asynchronously as they are needed. However, we are choosing to create const variables on the top to import them.

Why? If we return the import statements directly in the computed property, every time this is executed will return a new function. If later on you want to use the <keep-alive> element (which we will look at in the next section), your state will not be preserved.

The userComponent computed property was refactored, it now returns a function with an import statement in each case. This function itself returns a promise that will resolve into the component, which is … drumroll

An Object! Remember at the beginning of the article when we were discussing that :is can accept a String or Object? This is how you use :is—the name’s Object, Component Object.

By the way, if you don’t care for the arrow function without return syntax, or still struggle understanding what exactly is happening there (it took me a bit, I confess), you can rewrite the return statement like this:

const User = function() {
  return import('./components/Guest')
}

A smol bit worth mentioning about working with <component> is the special element <keep-alive>.

There will be times where you will want your user to switch between different elements inside your application. Imagine if in our demo scenario, the Member also had access to a button to switch to the User view for some extra functionality.

You could simply add some extra logic to your computed property to switch between them with the click of a button, but when the user starts using each component and jumping back and forth, they’re going to meet with a very particular problem—Vue is destroying and re-mounting the components as the user switches between them. Any state that is being stored locally in the component is going to be completely lost.

For these type of scenarios where you want to keep the components alive, you have this tool on your dev-belt. Let’s look at it on our current example.

<keep-alive>
  <component :is="userComponent"/>
</keep-alive>

By simply wrapping our <component> inside <keep-alive>, Vue will start caching and preserving the state of these components as they get swapped on the screen.

Keep in mind that, as we mentioned earlier, if you return the import function directly in the computed property, the state will not be cached due to how JavaScript comparing works. Simply put, functions will not be the exact same.

Let’s set up App.vue so that we can easily switch between components for testing.

<template>
  <div id="app">
    <keep-alive>
      <component :is="userComponent"/>
    </keep-alive>
    <br>
    <button @click="user = null">Guest</button>
    <button @click="user = {}">User</button>
    <button @click="user = {subscription: true}">Member</button>
  </div>
</template>

<script>
const Guest = () => import("./components/Guest");
const User = () => import("./components/User");
const Member = () => import("./components/Member");

export default {
  data() {
    return {
      user: null
    };
  },
  computed: {
    userComponent() {
      if (!this.user) return Guest;
      if (this.user && !this.user.subscription) return User;
      if (this.user && this.user.subscription) return Member;
    }
  }
};
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Additionally, make some changes to User.vue to add a basic internal state to test this out.

<template>
  <div>
    <div>User component {{ count }}</div>
    <br>
    <button @click="count++">MOAR!</button>
  </div>
</template>

<script>
export default {
  name:'User',
  data() {
    return {
      count: 0
    }
  }
}
</script>

If you click the MOAR button now and increase the counter and switch between the different components, you should be able to see that the state is being correctly preserved for the user.

Wrapping Up

Dynamic components open up an endless number of possibilities for structuring your application. You have also learned how to asynchronously import components into your application, which adds a layer of flexibility to what you can achieve.

If you want to check out the full code for this article, here’s a codesandbox.

As always, thanks for reading and share with me your experiences with dynamic components on Twitter at: @marinamosti.

P.S. All hail the magical avocado! 🥑

P.P.S. ❤️🔥🐶☠️


Marina Mosti_2020
About the Author

Marina Mosti

Marina Mosti is a full-stack web developer with over 13 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community.

She currently holds a position as Lead FE Developer at VoiceThread, and she is the author of the FormVueLatte library as well as a member of the Vuelidate team. In her spare time, she enjoys playing bass, drums, and videogames.

Related Posts

Comments

Comments are disabled in preview mode.