Blog

Register Vue components automatically

02 Sep, 2022
Xebia Background Header Wave

Vue components need to be registered before you can use them. This requires a lot of code that you have to maintain. Use unplugin-vue-components plugin to register components automatically 😅.
I’ll even tell you how this trick can be applied in Storybook and Jest/Vitest as well.

The problem

Let’s say we have this <app-button> component that renders an <app-icon>.

The Button component template doesn’t know about app-icon, until you register it. Else it thinks that app-icon is a custom component. In our case it is a Vue component, so we have to register it.

The same goes for app-button in the App component template.

<script setup lang="ts">
// SFC setup function automatically registers imported components
import AppButton from './Button.vue';'
</script>

<template>
  <app-button icon="thumbs-up">Click me</app-button>
</template>
<script lang="ts" setup>
import AppIcon from './Icon.vue';

defineProps<{ icon: string }>();
</script>

<template>
  <button v-bind="$attrs"><app-icon :name="icon" /> <slot></slot></button>
</template>

<style scoped>
button {
  border: 1px solid #42b883;
  border-radius: 6px;
  padding: 0.25rem 0.5rem;
}
</style>

Here is the same code but then in StackBlitz so that you can fiddle around yourself. (Embedded review only works in Chrome).

As your application grows, it will become a maintenance burden to register all those components locally when you use them. This is especially the case when these are shared common components (UI library) that are used in many places.

Registering components globally

The first thing you would probably do is to register these components globally.

import { createApp } from 'vue';
import App from './App.vue';
import Button from './Button.vue';
import Icon from './Icon.vue'; 

const app = createApp(App);

app
  .component('AppButton', Button)
  .component('AppIcon', Icon);

app.mount('#app')

Although you do not have to register the component many times but just once, you still have to manually maintain a list of component imports and registrations 😒.

Let’s refactor this a bit:

export { default as Button } from './Button.vue';
export { default as Icon } from './Icon.vue';
import { createApp } from 'vue';
import App from './App.vue';
import * as components from './index';

const app = createApp(App);

for (const componentName in components) {
  app.component(<code>App${componentName}, components[componentName]);
}

app.mount('#app')

Now you only have to maintain a list of exports in index.ts, which is already better 😅.

Note that we could also automatically read all the component names from a directory but this is starting to get complex if you have components in subdirectories.

Now sprinkle some magic. We want this automatically to happen.

There is one big drawback of registering components globally: when you do not use the component anywhere, it is still registered and therefore bundled. So you have to manually keep track of which components are used within the application and not import/register all the available components.

A similar problem also happens when you use Bootstrap with SCSS. You have to manually maintain a list of components styles. In this case I would advise something like TailwindCSS (JIT mode or using PurgeCSS), so that unused code will not be in your bundle. This is called tree shaking or dead code elimination.

Another example is Angular dependency injection. If you register services or components that are not used, you do not get a warning. It often happens when you configure TestBed in a unit test.

More on this in a future blog post.

The best global solution is to register the components automatically. If during compilation a custom html element is found in a template, in our case <app-button> and <app-icon>, it adds the registration statements for you.

There is a Vue plugin that takes care of this. It is called unplugin-vue-components. Here I’m using Vite, but for Vue CLI, Rollup or Webpack the config is pretty much the same.

This is how it works:

import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    Components({
      resolvers: [
          (name) => {
            if (name.startsWith('App')) {
              return { name: <code>App${name}, from: '@/' };
            }
          },
        ],
    }),
  ],
});

For any custom element it will trigger the callback. So for <app-button> the name will have the value AppButton. Here we do only match on component names that start with App. Tweak the pattern matching to your needs.

It will import from index.ts in the src directory, because the code>@/ alias is by default set up as ./src. Relative imports do not work, because unplugin-vue-components generates import statements locally in the component scripts.

If you want to automatically register components from third party libraries such as Vuetify or HeadlessUI, it will exactly work the some. There are even preset resolvers that you can use.

What about Storybook or Jest/Vitest?

Storybook and Jest have their own separate compilation process, so you have to redo all the setup code to make this work.

Storybook is by default based on Webpack (4 or 5). So you could (re)use the same unplugin-vue-components config as above, in a separate webpack.config.js file for Storybook.
I would even recommend to use the vite builder for Storybook, so that you leverage everything from Vite of you app also uses Vite. It will serve/build much faster than Webpack anyway.

In Jest you would probably use Vue Test Utils or Testing Library. There is a global.components setting to register components globally. You can do this per test or for your entire test suite.

The components that you don’t want to render in your tests are stubbed (or use shallowMount, but I don’t advise that).

Similar to Storybook, there is also a unit test alternative that leverages Vite. It is called Vitest.
It compiles using Vite (= esbuild which is super fast ⚡️), so you can reuse your vite config including the unplugin-vue-components configuration to automatically load the components.

⚠️ Note: I found a bug in unplugin-vue-components that stubs don’t work anymore in Vitest, so use with caution. If it fails for you, just use the "forloop technique" as demonstrated above. The larger bundle size downside is not relevant when running unit tests.

Frank van Wijk
Frank is a senior frontend consultant focusing on quality and maintainability.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts