Introduction to Components
Components are the building blocks of Vue applications. A component is a reusable, self-contained piece of code that encapsulates HTML, CSS, and JavaScript functionality. Components allow us to build complex user interfaces by composing them from smaller, focused pieces.
Real-world analogy: Components are like LEGO blocks. Each block has a specific purpose and can connect to other blocks. You can build complex structures by combining simple blocks in various ways. Similarly, Vue components are modular pieces that you can combine to create sophisticated applications.
Component hierarchy in a typical Vue application
Why Use Components?
Components provide several key benefits to modern web development:
Reusability
Once you create a component, you can reuse it throughout your application or even across projects. This prevents code duplication and promotes consistency.
Example: A button component with standard styling and behavior can be reused across the entire application, ensuring consistent look and feel.
Maintainability
Components break down complex UIs into manageable, focused pieces. Each component is responsible for a specific part of the UI, making the codebase easier to understand and maintain.
Example: Instead of a 500-line file handling an entire page, you might have 10 components of 50 lines each, with clear responsibilities.
Encapsulation
Components encapsulate their functionality, keeping their internal logic and state isolated from the rest of the application. This reduces side effects and makes debugging easier.
Example: A dropdown component manages its own open/closed state without affecting other parts of the application.
Composition
Complex UIs can be built by composing simpler components, allowing for a logical organization of your application's structure.
Example: A shopping cart page might be composed of cart item components, which themselves contain product image components, quantity selectors, and price displays.
Component Registration
Before using a component in Vue, you need to register it. There are two ways to register components:
Global Registration
Global components are available throughout your application without needing to explicitly import them into each component where they're used.
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import ButtonComponent from './components/ButtonComponent.vue'
const app = createApp(App)
// Register globally
app.component('button-component', ButtonComponent)
app.mount('#app')
Pros: Easy to use anywhere; no need to import in individual components.
Cons: All globally registered components are included in the final bundle even if they aren't used, potentially increasing bundle size. They also make dependency relationships less explicit.
Local Registration
Local components are registered within the specific components that use them. This approach is generally preferred for most components.
// ParentComponent.vue
import ButtonComponent from './ButtonComponent.vue'
export default {
components: {
ButtonComponent
// or with a different name: 'my-button': ButtonComponent
},
// rest of the component definition...
}
Pros: Makes dependencies explicit; supports tree-shaking for better bundle size optimization.
Cons: Requires importing in each component where it's used.
Best Practice:
Use local registration for most components to maintain clear dependency relationships and optimize bundle size. Use global registration sparingly for truly ubiquitous components like basic UI elements or for plugins that register their own components.
Component Types and Definition Formats
Vue offers different ways to define components, each with its own use cases and advantages.
Single-File Components (SFCs)
The most common and recommended approach for Vue applications is using Single-File Components,
which have a .vue file extension. SFCs contain the template, script, and style
for a component in a single file.
<!-- ButtonComponent.vue -->
<template>
<button class="custom-button" @click="handleClick">
{{ text }}
</button>
</template>
<script>
export default {
props: {
text: {
type: String,
default: 'Click me'
}
},
methods: {
handleClick() {
this.$emit('button-click')
}
}
}
</script>
<style scoped>
.custom-button {
background-color: #42b883;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
}
.custom-button:hover {
background-color: #35495e;
}
</style>
Benefits of SFCs:
- Collocation of related code (HTML, CSS, JavaScript) in one file
- Syntax highlighting for all three languages
- Scoped CSS to prevent style leakage
- More maintainable as applications grow
- Better editor/IDE support
JavaScript Object Components
For simpler use cases or when not using a build system, you can define components as JavaScript objects directly:
// Define a component using JavaScript object
const ButtonComponent = {
template: `
<button class="custom-button" @click="handleClick">
{{ text }}
</button>
`,
props: {
text: {
type: String,
default: 'Click me'
}
},
methods: {
handleClick() {
this.$emit('button-click')
}
}
}
Component Definition Styles in Vue 3
Vue 3 introduced a new Composition API alongside the traditional Options API. Both can be used in SFCs and JavaScript object components.
Options API
<script>
export default {
// Component state
data() {
return {
count: 0
}
},
// Computed properties
computed: {
doubleCount() {
return this.count * 2
}
},
// Methods
methods: {
increment() {
this.count++
}
},
// Lifecycle hooks
mounted() {
console.log('Component mounted')
}
}
</script>
Organizes code by option types (data, methods, computed, etc.). Familiar to Vue 2 developers and often easier for beginners.
Composition API
<script setup>
import { ref, computed, onMounted } from 'vue'
// Component state
const count = ref(0)
// Computed properties
const doubleCount = computed(() => count.value * 2)
// Methods
function increment() {
count.value++
}
// Lifecycle hooks
onMounted(() => {
console.log('Component mounted')
})
</script>
Organizes code by logical concern rather than option type. Better TypeScript support and more flexible for complex components.
Component Templates
The template defines a component's HTML structure and how it renders to the DOM. Vue offers different ways to define templates.
Template in Single-File Components
In an SFC, the template is defined in the <template> section:
<template>
<div class="user-profile">
<img :src="user.avatar" :alt="user.name">
<h2>{{ user.name }}</h2>
<p>{{ user.bio }}</p>
</div>
</template>
Template in JavaScript
For JavaScript object components, templates can be defined as strings:
const UserProfile = {
template: `
<div class="user-profile">
<img :src="user.avatar" :alt="user.name">
<h2>{{ user.name }}</h2>
<p>{{ user.bio }}</p>
</div>
`,
// other options...
}
Render Functions
For advanced use cases, Vue allows defining the component's output using render functions instead of templates:
import { h } from 'vue'
export default {
props: ['user'],
render() {
return h('div', { class: 'user-profile' }, [
h('img', { src: this.user.avatar, alt: this.user.name }),
h('h2', this.user.name),
h('p', this.user.bio)
])
}
}
When to use render functions: For programmatically generated content, complex conditional rendering, or when building reusable component libraries that need maximum flexibility.
Template Features
Vue templates support all the template syntax we covered in the previous lecture:
- Text interpolation with
{{ expression }} - Attribute binding with
v-bindor: - Event handling with
v-onor@ - Conditional rendering with
v-if,v-else-if, andv-else - List rendering with
v-for - Two-way binding with
v-model - Dynamic components with
<component :is="..."> - Slots for content distribution
Component Lifecycle
Each Vue component instance goes through a series of initialization steps when it's created. This is called the component lifecycle. Vue provides lifecycle hooks that let you run code at specific stages of this process.
Main Lifecycle Hooks
export default {
// Called before the component instance is initialized
beforeCreate() {
console.log('Component is being created')
// Data and events are not yet set up
},
// Called after the instance is created
created() {
console.log('Component is created')
// Data is reactive, events are active
// Good place to make API calls
this.fetchData()
},
// Called before the component is mounted to the DOM
beforeMount() {
console.log('Component is about to be mounted')
},
// Called after the component is mounted to the DOM
mounted() {
console.log('Component is mounted')
// DOM is accessible
// Good place to initialize libraries that need DOM access
this.initializeChart()
},
// Called before the component updates due to data changes
beforeUpdate() {
console.log('Component is about to update')
},
// Called after the component has updated
updated() {
console.log('Component has updated')
// DOM is re-rendered with new data
},
// Called before the component is unmounted/removed
beforeUnmount() {
console.log('Component is about to be unmounted')
// Good place to clean up resources
this.destroyChart()
},
// Called after the component is unmounted/removed
unmounted() {
console.log('Component is unmounted')
// Component is completely destroyed
}
}
Practical Usage of Lifecycle Hooks
export default {
data() {
return {
chartInstance: null,
dataLoaded: false,
users: []
}
},
// Best for data fetching
created() {
// API calls, initialize non-DOM data
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => {
this.users = data
this.dataLoaded = true
})
},
// Best for DOM interactions
mounted() {
// Initialize libraries that need DOM access
import('chart.js').then(Chart => {
const ctx = this.$refs.chart.getContext('2d')
this.chartInstance = new Chart(ctx, {
// chart configuration
})
})
// Add event listeners
window.addEventListener('resize', this.handleResize)
},
// Clean up resources
beforeUnmount() {
// Destroy any objects to prevent memory leaks
if (this.chartInstance) {
this.chartInstance.destroy()
}
// Remove event listeners
window.removeEventListener('resize', this.handleResize)
}
}
Lifecycle Hooks with Composition API
In the Composition API, lifecycle hooks are imported functions:
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
const chartInstance = ref(null)
onMounted(() => {
// Similar to mounted() in Options API
import('chart.js').then(Chart => {
const ctx = document.getElementById('chart').getContext('2d')
chartInstance.value = new Chart(ctx, {
// chart configuration
})
})
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
// Similar to beforeUnmount() in Options API
if (chartInstance.value) {
chartInstance.value.destroy()
}
window.removeEventListener('resize', handleResize)
})
function handleResize() {
// Resize logic here
}
</script>
Component Communication
Vue components need to communicate with each other. The most basic form of communication is between parent and child components, which we'll explore in greater detail in our next lecture.
Parent-Child Communication
- Props (Parent → Child): Data passed from parent to child
- Events (Child → Parent): Child component emits events that parent listens for
Communication Between Siblings or Distant Components
- Event Bus: A common object used to emit and listen for events (deprecated in Vue 3)
- Vuex/Pinia: Centralized state management (covered in a future lecture)
- Provide/Inject: For passing data deeply through the component tree
Dynamic Components
Vue provides a special <component> element that allows switching between
different components dynamically.
<template>
<div class="tab-container">
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab.name"
:class="{ active: currentTab === tab.name }">
{{ tab.label }}
</button>
</div>
<!-- Dynamic component based on currentTab -->
<component :is="currentTabComponent" class="tab-content"></component>
</div>
</template>
<script>
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
import SettingsTab from './tabs/SettingsTab.vue'
export default {
components: {
HomeTab,
ProfileTab,
SettingsTab
},
data() {
return {
currentTab: 'home',
tabs: [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
}
},
computed: {
currentTabComponent() {
// Convert 'home' to 'HomeTab', etc.
return this.currentTab.charAt(0).toUpperCase() +
this.currentTab.slice(1) + 'Tab'
}
}
}
</script>
KeepAlive for Component State Preservation
When switching between components with <component :is>, the component is
destroyed and recreated by default. To preserve their state, you can wrap them in a <keep-alive> component:
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
Real-world use case: This is particularly useful for preserving form input data when users switch between tabs, or for maintaining scroll position in lists.
KeepAlive Hooks
Components inside <keep-alive> get access to two additional lifecycle hooks:
export default {
// Called when a kept-alive component is reactivated
activated() {
console.log('Component activated')
// Good place to fetch fresh data
this.refreshData()
},
// Called when a kept-alive component is deactivated
deactivated() {
console.log('Component deactivated')
// Good place to pause expensive operations
this.pauseVideoPlayback()
}
}
Practical Component Example: Reusable Modal
Let's create a reusable modal component to illustrate the component concept:
// ModalComponent.vue
<template>
<transition name="modal-fade">
<div v-if="isVisible" class="modal-overlay" @click="closeOnBackdrop ? close() : null">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="modal-close" @click="close">×</button>
</div>
<div class="modal-body">
<slot>Default content if no slot content provided</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button class="modal-button cancel" @click="close">Cancel</button>
<button class="modal-button confirm" @click="confirm">Confirm</button>
</slot>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'ModalComponent',
props: {
title: {
type: String,
default: 'Modal Title'
},
isVisible: {
type: Boolean,
default: false
},
closeOnBackdrop: {
type: Boolean,
default: true
}
},
methods: {
close() {
this.$emit('close')
},
confirm() {
this.$emit('confirm')
}
},
created() {
// Add ESC key listener to close modal
const handleEsc = (event) => {
if (event.key === 'Escape' && this.isVisible) {
this.close()
}
}
document.addEventListener('keydown', handleEsc)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keydown', handleEsc)
})
}
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.modal-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
width: 500px;
max-width: 90%;
max-height: 90%;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex-grow: 1;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.modal-button {
padding: 8px 15px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.modal-button.cancel {
background-color: #f5f5f5;
}
.modal-button.confirm {
background-color: #42b883;
color: white;
}
/* Transition animations */
.modal-fade-enter-active, .modal-fade-leave-active {
transition: opacity 0.3s;
}
.modal-fade-enter-from, .modal-fade-leave-to {
opacity: 0;
}
</style>
Using the Modal Component
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<modal-component
title="Confirm Action"
:is-visible="showModal"
@close="showModal = false"
@confirm="handleConfirm">
<p>Are you sure you want to proceed with this action?</p>
<p>This cannot be undone.</p>
<!-- Custom footer using named slot -->
<template #footer>
<button class="custom-button" @click="showModal = false">No, Cancel</button>
<button class="custom-confirm-button" @click="handleConfirm">Yes, Proceed</button>
</template>
</modal-component>
</div>
</template>
<script>
import ModalComponent from './ModalComponent.vue'
export default {
components: {
ModalComponent
},
data() {
return {
showModal: false
}
},
methods: {
handleConfirm() {
// Handle the confirmation action
console.log('Action confirmed!')
this.showModal = false
}
}
}
</script>
This example illustrates several key component concepts:
- Component encapsulation (HTML, CSS, and JavaScript together)
- Props for customization (
title,isVisible,closeOnBackdrop) - Events for communication (
@close,@confirm) - Slots for content projection (default and named slots)
- Lifecycle hooks for setup and cleanup (ESC key handler)
- Transitions for animations
- Scoped CSS for component-specific styling
Activities for Practice
Exercise 1: Create a Button Component
Create a reusable button component with the following features:
- Props for customization: text, type (primary, secondary, danger), size (small, medium, large), disabled state
- Custom events for click and hover actions
- Slots for allowing custom content (icons, text)
- Styled appropriately based on the type and size props
Then, create a simple app that uses multiple instances of your button component with different configurations.
Exercise 2: Component Lifecycle Exploration
Create a component that demonstrates all the major lifecycle hooks. For each hook:
- Add a console.log statement with the hook name
- Display the hook name in the UI when it fires
- Add a simple action that's appropriate for that lifecycle stage
Create a parent component with a toggle button that mounts/unmounts this component so you can observe the complete lifecycle.
Exercise 3: Dynamic Tabs Component
Expand on the dynamic components example to create a full tab system:
- Create a Tabs container component
- Create a Tab component for individual tab content
- Implement tab switching with dynamic components
- Use keep-alive to preserve tab state
- Add the ability to add and remove tabs dynamically
For an extra challenge, implement a "drag to reorder" feature for the tabs.