Skip to content

VueJS - Components

Components are the cornerstone of building applications in Vue (and many other modern frameworks). They allow you to split the UI into independent, reusable pieces, each managing its own logic and markup. Think of them like custom HTML elements you can define and reuse throughout your application.

In Vue 3, components are typically defined as objects containing options (Options API) or using the setup function (Composition API). For better organization, especially in build environments, Single-File Components (.vue files) are the standard way to define components, colocating template, script, and style.

Let’s create a simple counter button component in a hypothetical CounterButton.vue file:

CounterButton.vue
<script setup>
import { ref } from 'vue';
// Reactive state specific to this component instance
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">
You clicked me {{ count }} times
</button>
</template>
<style scoped>
/* Styles specific to this component */
button {
background-color: lightgreen;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
button:hover {
background-color: darkgreen;
color: white;
}
</style>

<script setup> is compile-time syntactic sugar for using the Composition API inside SFCs. Variables and functions declared here are automatically exposed to the template. The scoped attribute on <style> ensures the CSS only applies to elements within this component.

Before you can use a component, it needs to be registered.

Globally registered components can be used in the template of any component within that application instance without needing to import them. Use app.component():

// main.js (Your application entry point)
import { createApp } from 'vue';
import App from './App.vue';
import GlobalComponent from './components/GlobalComponent.vue';
const app = createApp(App);
// Register 'GlobalComponent' globally
app.component('GlobalComponent', GlobalComponent);
// Now <GlobalComponent /> can be used anywhere in App or its children
app.mount('#app');

Global registration is convenient but can lead to large bundle sizes if many unused components are registered globally. It’s often better for generic, widely used components (like base buttons, inputs, modals).

Locally registered components are only available within the component where they are registered. This is the preferred approach for most application components as it makes dependency relationships explicit and works better with tree-shaking (removing unused code).

In <script setup>, simply importing a component makes it available for use in the template:

ParentComponent.vue
<script setup>
// Import the component
import LocalButton from './LocalButton.vue';
import AnotherComponent from './AnotherComponent.vue';
</script>
<template>
<div>
<p>Using a locally registered component:</p>
<!-- Use the imported component directly (PascalCase or kebab-case) -->
<LocalButton />
<another-component></another-component> <!-- kebab-case also works -->
</div>
</template>

If not using <script setup> (e.g., using Options API), you would use the components option:

// ParentComponent.vue (Options API example)
import LocalButton from './LocalButton.vue';
export default {
components: {
// Register the component locally
LocalButton
// You could also rename it: 'MyButton': LocalButton
}
// ... other options like data, methods
}

Components often need to receive data from their parent. This is done using props. Props are custom attributes you can register on a component. A parent component can then pass data down to the child by binding to these attributes.

Greeting.vue
<script setup>
// Define the props the component accepts
// 'defineProps' is a compiler macro, no import needed
const props = defineProps({
name: {
type: String, // Type validation
required: true, // Make the prop required
default: 'World' // Default value if not provided (though 'required' makes this less relevant)
}
});
// Props are accessible directly in the template,
// or via the 'props' object in script setup
console.log('Greeting received name:', props.name);
</script>
<template>
<h2>Hello, {{ name }}!</h2>
</template>

Using the Greeting component in a parent:

App.vue
<script setup>
import { ref } from 'vue';
import Greeting from './Greeting.vue';
const userName = ref('Alice');
</script>
<template>
<div>
<!-- Pass static value -->
<Greeting name="Bob" />
<!-- Bind to dynamic value using v-bind (or :) -->
<Greeting :name="userName" />
<input v-model="userName" placeholder="Change name"/>
</div>
</template>

Props allow for one-way data flow: parent passes data down to the child. The child should not directly mutate props.

Emitting Events (Child-to-Parent Communication)

Section titled “Emitting Events (Child-to-Parent Communication)”

Since children shouldn’t modify parent state directly via props, they communicate upwards by emitting custom events. The parent component listens for these events using v-on (or @).

Example: Custom Input Component Emitting ‘update’

Section titled “Example: Custom Input Component Emitting ‘update’”
CustomInput.vue
<script setup>
// Define props (e.g., modelValue for v-model compatibility)
const props = defineProps(['modelValue']);
// Define the events the component can emit
// 'defineEmits' is a compiler macro
const emit = defineEmits(['update:modelValue', 'focused']);
function handleInput(event) {
// Emit the 'update:modelValue' event with the new value
// This pattern allows the component to work with v-model
emit('update:modelValue', event.target.value);
}
function handleFocus() {
emit('focused'); // Emit a custom 'focused' event
}
</script>
<template>
<input
type="text"
:value="modelValue"
@input="handleInput"
@focus="handleFocus"
placeholder="Custom Input"
/>
</template>

Using CustomInput in a parent:

App.vue
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const textValue = ref('');
function onInputFocus() {
console.log('Custom input was focused!');
}
</script>
<template>
<div>
<p>Current Value: {{ textValue }}</p>
<!-- Use v-model (which listens for 'update:modelValue' event) -->
<CustomInput v-model="textValue" @focused="onInputFocus" />
<!-- Explicitly listening (equivalent to v-model): -->
<!-- <CustomInput
:modelValue="textValue"
@update:modelValue="newValue => textValue = newValue"
@focused="onInputFocus" /> -->
</div>
</template>

The child emits events (update:modelValue, focused), and the parent listens (v-model, @focused) to react accordingly.

Sometimes you need to switch between different components dynamically. Vue provides the special <component> element with the is attribute for this purpose.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Dynamic Components</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
.tab-button { padding: 6px 10px; border-bottom: 2px solid #ccc; cursor: pointer; background: #f0f0f0; margin-right: -1px;}
.tab-button:hover { background: #e0e0e0; }
.tab-button.active { background: #e0e0e0; border-bottom-color: green; }
.tab-content { border: 1px solid #ccc; padding: 10px; }
</style>
</head>
<body>
<div id="app">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-button', { active: currentTab === tab.component }]"
@click="currentTab = tab.component"
>
{{ tab.name }}
</button>
<!-- Dynamic component is rendered here -->
<div class="tab-content">
<!-- Use keep-alive to preserve state when switching -->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
</div>
</div>
<script>
const { createApp, ref, shallowRef, computed } = Vue;
// Define some simple components (could be imported from .vue files)
const TabHome = { template: '<p>Home component content</p>' };
const TabPosts = { template: '<p>Posts component content</p>' };
const TabArchive = { template: '<p>Archive component content</p>' };
const app = createApp({
setup() {
const tabs = shallowRef([ // Use shallowRef for non-deep reactivity on the array itself
{ id: 1, name: 'Home', component: 'TabHome' },
{ id: 2, name: 'Posts', component: 'TabPosts' },
{ id: 3, name: 'Archive', component: 'TabArchive' }
]);
const currentTab = ref('TabHome'); // Holds the *name* of the current component
// Computed property to resolve the actual component object
const currentTabComponent = computed(() => {
switch (currentTab.value) {
case 'TabHome': return TabHome;
case 'TabPosts': return TabPosts;
case 'TabArchive': return TabArchive;
default: return TabHome;
}
});
return { tabs, currentTab, currentTabComponent };
}
});
// Register components (globally for this simple example)
app.component('TabHome', TabHome);
app.component('TabPosts', TabPosts);
app.component('TabArchive', TabArchive);
app.mount('#app');
</script>
</body>
</html>

In this tabbed interface example:

  • The currentTab ref stores the name of the component to display.
  • A computed property currentTabComponent resolves the name to the actual component object.
  • <component :is="currentTabComponent"> renders the component determined by the computed property.
  • Clicking the buttons changes currentTab, which updates currentTabComponent, causing the <component> element to render the new component.
  • <keep-alive> is used to cache the inactive components so their state is preserved when switching back and forth.

Components are fundamental to building scalable and maintainable Vue applications. Mastering props, events, and component composition is key to becoming proficient with Vue.

Further Reading: Vue’s official documentation on Components Basics: https://vuejs.org/guide/essentials/component-basics.html and Component In-Depth: https://vuejs.org/guide/components/registration.html