Component Basics
Defining Components
In Vue 3, components can be defined using ES6 module syntax:
// MyComponent.vue
<template>
<div class="my-component">
<h1>{{ message }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello from MyComponent'
};
}
};
</script>
<style scoped>
.my-component {
color: blue;
}
</style>
Registering Components
Components can be registered globally or locally. Global registration makes the component available throughout the application, while local registration limits it to the current component and its children.
// Global Registration
import MyComponent from './MyComponent.vue';
const app = createApp(App);
app.component('MyComponent', MyComponent);
app.mount('#app');
// Local Registration
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent
}
};
Using Components
Once registered, components can be used in templates:
<template>
<div>
<MyComponent />
</div>
</template>
Component Props
Components can receive data from their parent via props:
// MyComponent.vue
export default {
props: {
title: String
},
template: '<h1>{{ title }}</h1>'
};
// Using the Component
<template>
<MyComponent :title="pageTitle" />
</template>
<script>
export default {
data() {
return {
pageTitle: 'Page Title'
};
}
};
</script>
Custom Events
Components can emit custom events using the emit method, which parent components can listen to:
// MyComponent.vue
export default {
methods: {
emitEvent() {
this.$emit('custom-event', 'Hello from child');
}
},
template: '<button @click="emitEvent">Emit Event</button>'
};
// Using the Component
<template>
<MyComponent @custom-event="handleEvent" />
</template>
<script>
export default {
methods: {
handleEvent(data) {
console.log(data);
}
}
};
</script>
Component Communication: Props, Emits, Provide/Inject
Parent-Child Communication
Parent-child communication is the most common form of component interaction, involving:
- Props Downward: Parents pass data to children.
- Custom Events Upward: Children send information to parents.
- v-model: Two-way binding, often used for form controls.
Props Downward
// Parent Component
<template>
<ChildComponent :message="parentMessage" />
</template>
<script>
export default {
data() {
return {
parentMessage: 'Hello from parent!'
};
}
};
</script>
// Child Component
export default {
props: ['message'],
template: `<p>{{ message }}</p>`
};
Custom Events Upward
// Child Component
export default {
methods: {
sendMessage() {
this.$emit('send-message', 'Hello from child!');
}
},
template: `<button @click="sendMessage">Send Message</button>`
};
// Parent Component
<template>
<ChildComponent @send-message="receiveMessage" />
</template>
<script>
export default {
methods: {
receiveMessage(message) {
console.log(message);
}
}
};
</script>
v-model
// Parent Component
<template>
<ChildComponent v-model="inputValue" />
</template>
<script>
export default {
data() {
return {
inputValue: ''
};
}
};
</script>
// Child Component
export default {
inheritAttrs: false,
props: {
modelValue: String
},
emits: ['update:modelValue'],
template: `
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
`
};
Sibling Communication
Siblings cannot communicate directly and typically rely on:
- Parent as Intermediary: Using the parent component to relay data.
- Ref and Provide/Inject: Providing data in an ancestor and injecting it in descendants.
- Event Bus: Using libraries like
mittor a custom event bus.
Parent as Intermediary
// Parent Component
<template>
<ChildA @send-data="handleData" />
<ChildB :data="receivedData" />
</template>
<script>
export default {
data() {
return {
receivedData: null
};
},
methods: {
handleData(data) {
this.receivedData = data;
}
}
};
</script>
Using Ref and Provide/Inject
// Ancestor Component
export default {
provide() {
return {
sharedData: this.sharedData
};
},
data() {
return {
sharedData: 'Shared Data'
};
}
};
// Descendant Component
export default {
inject: ['sharedData'],
template: `<p>{{ sharedData }}</p>`
};
Cross-Component Communication
Cross-component communication, for components without direct parent-child relationships, uses:
- Vuex: A global state management library.
- Pinia: A lightweight alternative to Vuex.
- Event Bus: Libraries like
mitt.
Vuex
// store/index.js
import { createStore } from 'vuex';
export default createStore({
state: {
counter: 0
},
mutations: {
increment(state) {
state.counter++;
}
},
actions: {
increment({ commit }) {
commit('increment');
}
}
});
// Component Usage
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions(['increment'])
}
};
Pinia
// store/index.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++;
}
}
});
// Component Usage
import { useCounterStore } from './store';
export default {
setup() {
const store = useCounterStore();
store.increment();
}
};
Event Bus
// src/bus.js
import mitt from 'mitt';
export const eventBus = mitt();
// Emit Event
eventBus.emit('someEvent', payload);
// Listen to Event
eventBus.on('someEvent', listener);
Cell Communication
Though not a standard communication pattern, cells in tables or grids may need to interact. This is typically achieved by treating cells as child components and using parent-child communication methods.
Slots and Scoped Slots
Slots Overview
Slots are Vue’s mechanism for content distribution, allowing child components to reserve placeholders that parent components can fill with content. In Vue 3, slot syntax is simplified, replacing slot and slot-scope with the v-slot directive.
Default Slots
Default slots are the simplest type, where unspecified content is treated as the default slot’s content.
<!-- Child Component -->
<template>
<div>
<slot></slot>
</div>
</template>
<!-- Parent Component -->
<template>
<ChildComponent>
<!-- Content inserted into the default slot -->
<p>This is the default slot content.</p>
</ChildComponent>
</template>
Named Slots
Named slots allow child components to define multiple slots with specific names, filled by parents using the v-slot directive with the slot name.
<!-- Child Component -->
<template>
<div>
<header><slot name="header"></slot></header>
<main><slot name="main"></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
</template>
<!-- Parent Component -->
<template>
<ChildComponent>
<template v-slot:header>
<!-- Inserted into the header slot -->
<h1>Page Title</h1>
</template>
<template v-slot:main>
<!-- Inserted into the main slot -->
<p>This is the main content.</p>
</template>
<template v-slot:footer>
<!-- Inserted into the footer slot -->
<p>Copyright © 2023</p>
</template>
</ChildComponent>
</template>
Scoped Slots
Scoped slots allow parent components to access data or methods from the child component, using the v-slot directive to receive the data.
<!-- Child Component -->
<template>
<div>
<slot v-bind:post="post"></slot>
</div>
</template>
<script>
export default {
data() {
return {
post: {
title: 'Vue 3 Slots Tutorial',
author: 'John Doe'
}
};
}
};
</script>
<!-- Parent Component -->
<template>
<ChildComponent>
<template v-slot="{ post }">
<!-- Accesses the post data from the child -->
<h1>{{ post.title }}</h1>
<p>Author: {{ post.author }}</p>
</template>
</ChildComponent>
</template>
Named Scoped Slots
Named scoped slots combine named slots and scoped slots, allowing parents to specify slot names while accessing child data.
<!-- Child Component -->
<template>
<div>
<slot name="post" v-bind:post="post"></slot>
</div>
</template>
<script>
export default {
data() {
return {
post: {
title: 'Vue 3 Slots Tutorial',
author: 'John Doe'
}
};
}
};
</script>
<!-- Parent Component -->
<template>
<ChildComponent>
<template v-slot:post="{ post }">
<h1>{{ post.title }}</h1>
<p>Author: {{ post.author }}</p>
</template>
</ChildComponent>
</template>
Shorthand Syntax
When v-slot has no expression, the # symbol can be used as a shorthand.
<!-- Shorthand Syntax -->
<template>
<ChildComponent #default="{ prop }">
<!-- Slot content -->
</ChildComponent>
</template>
Best Practices
- Clear Slot Purpose: Document each slot’s purpose to aid other developers.
- Use Named Slots: For components with multiple slots, named slots enhance clarity.
- Scoped Slots for Flexibility: Use scoped slots when parent components need access to child data, avoiding hard-coded data and improving reusability.
Dynamic and Asynchronous Components
Dynamic Components
Dynamic components enable runtime switching of components using the <component> tag and the is attribute to specify the rendered component.
Basic Usage
<template>
<div>
<component :is="currentComponent"></component>
<button @click="switchComponent">Switch Component</button>
</div>
</template>
<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
export default {
data() {
return {
currentComponent: ComponentA
};
},
methods: {
switchComponent() {
this.currentComponent = this.currentComponent === ComponentA ? ComponentB : ComponentA;
}
}
};
</script>
Asynchronous Components
Asynchronous components delay loading until needed, reducing initial load times and improving user experience.
Using defineAsyncComponent
Vue 3’s defineAsyncComponent function creates asynchronous components, accepting a factory function that returns a Promise resolving to the component definition.
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
Configuring Loading, Error, and Delay
defineAsyncComponent accepts a configuration object with loading, error, and delay options to manage loading states, errors, and delays.
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200, // Show loading component after 200ms
timeout: 3000 // Show error component if loading exceeds 3000ms
});
Combining Dynamic and Asynchronous Components
Dynamic and asynchronous components can be combined for on-demand loading and dynamic switching.
<template>
<div>
<component :is="currentComponent"></component>
<button @click="switchComponent">Switch Component</button>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue';
const ComponentA = defineAsyncComponent(() => import('./ComponentA.vue'));
const ComponentB = defineAsyncComponent(() => import('./ComponentB.vue'));
export default {
data() {
return {
currentComponent: ComponentA
};
},
methods: {
switchComponent() {
this.currentComponent = this.currentComponent === ComponentA ? ComponentB : ComponentA;
}
}
};
</script>
Best Practices
- Lazy Loading: Use asynchronous components to reduce initial load times.
- Error Handling: Ensure proper error handling for failed component loads.
- Performance Optimization: Leverage code splitting and Webpack chunk loading for better performance.
- Cache Component Instances: Cache instances for frequently switched components to avoid unnecessary re-renders.
Built-in Components
<component>
The <component> tag is an abstract component for dynamically rendering components or elements based on the is attribute.
<template>
<div>
<component :is="currentComponent"></component>
</div>
</template>
<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
export default {
data() {
return {
currentComponent: ComponentA
};
}
};
</script>
<transition> and <transition-group>
<transition> and <transition-group> enable transition and animation effects. <transition> handles single elements or components, while <transition-group> manages groups of elements.
<template>
<transition name="fade">
<p v-if="show">Hello World</p>
</transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<keep-alive>
<keep-alive> is an abstract component that preserves component state and avoids re-rendering by caching inactive component instances.
<template>
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
</template>
<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';
export default {
data() {
return {
currentView: ComponentA
};
}
};
</script>
<slot>
<slot> facilitates content insertion, allowing parent components to pass content to specific slot locations in child components for flexibility and reusability.
<!-- Child Component -->
<template>
<div>
<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>
</div>
</template>
<!-- Parent Component -->
<template>
<ChildComponent>
<template v-slot:header>
<h1>Header Content</h1>
</template>
<template v-slot:content>
<p>Main Content</p>
</template>
<template v-slot:footer>
<p>Footer Content</p>
</template>
</ChildComponent>
</template>
<teleport>
<teleport>, introduced in Vue 3, renders components to a different DOM location, useful for modals or fixed-position elements.
<template>
<teleport to="#modal-root">
<div v-if="isOpen" class="modal">
<p>This is a modal dialog.</p>
</div>
</teleport>
</template>
<Suspense>
<Suspense>, new in Vue 3, manages loading states for asynchronous components with default and fallback slots for loaded and loading content, respectively.
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<SuspenseBoundary>
<SuspenseBoundary> is an experimental Vue 3 feature for fine-grained control over Suspense boundaries and asynchronous component loading behavior.
<Inherited>
<Inherited> is an experimental Vue 3 feature for inheriting attributes or state from parent components, enhancing state propagation flexibility in component trees.
Composables
Core Concepts
- Composables: Functions encapsulating specific functionality for reuse across components.
- setup() Function: The entry point for Composition API, defining component logic and state.
- Reactive System: Vue 3’s reactivity system enables reactive data and computed properties.
- Lifecycle Hooks: Traditional lifecycle hooks are replaced by composable functions like
onMountedandonUnmountedin the Composition API.
Reactive System
Vue 3’s reactivity system uses wrappers like ref, reactive, and computed to make data reactive, automatically updating dependent views on changes.
import { ref } from 'vue';
const count = ref(0); // Creates a reactive reference
const doubleCount = computed(() => count.value * 2); // Creates a reactive computed property
Composable Example
For a component handling counter logic, extract it into a composable:
import { ref, onMounted } from 'vue';
function useCounter(initialCount = 0) {
const count = ref(initialCount);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
onMounted(() => {
console.log('Counter initialized');
});
return {
count,
increment,
decrement
};
}
Using Composables in Components
import { defineComponent } from 'vue';
import { useCounter } from './useCounter';
export default defineComponent({
setup() {
const { count, increment, decrement } = useCounter(10);
return {
count,
increment,
decrement
};
}
});
Reusing Composables
Composables, being independent functions, are easily reusable across components:
import { defineComponent } from 'vue';
import { useCounter } from './useCounter';
export default defineComponent({
setup() {
const { count, increment, decrement } = useCounter(5);
return {
count,
increment,
decrement
};
}
});
Combining Multiple Composables
Combine multiple composables in a component for complex logic:
import { defineComponent } from 'vue';
import { useCounter } from './useCounter';
import { useTimer } from './useTimer';
export default defineComponent({
setup() {
const { count, increment } = useCounter();
const { start, stop } = useTimer(increment);
return {
count,
start,
stop
};
}
});
Summary
- Single Responsibility: Each composable should handle one specific logic.
- Naming Conventions: Name composables to reflect functionality, e.g.,
useCounter,useFetchData. - Test-Friendly: Composables are easy to unit test as they are typically pure functions.
- Avoid Side Effects: Encapsulate side effects (e.g., network requests, DOM operations) in dedicated functions like
onMountedoronBeforeUnmount.
Custom Directives
Creating Custom Directives
Custom directives are registered using app.directive, which takes the directive name and an object with hook functions.
// Define a simple custom directive
import { app } from './app'; // Assume app is your Vue application instance
app.directive('focus', {
mounted(el) {
el.focus(); // Auto-focus the element when inserted into the DOM
}
});
Using Custom Directives
Once registered, custom directives can be used in templates like built-in directives:
<!-- Use custom directive -->
<input type="text" v-focus>
Custom Directive Hooks
Custom directives can include the following hooks:
mounted: Called when the bound element is inserted into the DOM.updated: Called when the bound element’s VNode updates, possibly before its children.unmounted: Called when the bound element is removed from the DOM.beforeUpdate: Called before the VNode updates.bind: Called once when the directive is first bound to the element.
app.directive('tooltip', {
bind(el, binding, vnode) {
// Initialize tooltip state
},
updated(el, binding, vnode) {
// Update tooltip content or position
},
unmounted(el) {
// Clean up tooltip event listeners
}
});
Passing Arguments to Directives
Custom directives can accept static or dynamic arguments:
<!-- Static argument -->
<div v-tooltip="'top'">Hover me</div>
<!-- Dynamic argument -->
<div v-tooltip="position">Hover me</div>
Access arguments via binding.value in the directive definition:
app.directive('tooltip', {
bind(el, binding) {
const position = binding.value; // Access the passed argument
// Set tooltip position
}
});
Using Modifiers
Custom directives support modifiers to declaratively alter behavior:
<!-- Use modifier -->
<button v-my-directive.modifier="value">Click me</button>
Access modifiers via binding.modifiers in the directive definition:
app.directive('my-directive', {
bind(el, binding) {
if (binding.modifiers.modifier) {
// Alter directive behavior
}
}
});
Using and Creating Plugins
Using Existing Plugins
Most Vue plugins follow a pattern with an install function that accepts the Vue application instance. In Vue 3, plugins are installed using app.use().
Example: Using Vuex
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
const app = createApp(App);
app.use(store);
app.mount('#app');
Creating Custom Plugins
A Vue plugin is a JavaScript object or function with an install method, which takes at least the Vue application instance as a parameter.
Example: Custom Logger Plugin
// plugins/logger.js
export default {
install(app, options) {
app.config.globalProperties.$log = console.log.bind(console);
}
};
Install the plugin in your main application file using app.use():
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import logger from './plugins/logger';
const app = createApp(App);
app.use(logger);
app.mount('#app');
Now, the $log method is available in any component:
// components/MyComponent.vue
export default {
methods: {
logMessage() {
this.$log('Hello, world!');
}
}
};
Plugin Options
The install method can accept a second parameter for plugin options, allowing configuration:
// plugins/myPlugin.js
export default {
install(app, options) {
app.config.globalProperties.$myPlugin = options.someOption;
}
};
// main.js
import myPlugin from './plugins/myPlugin';
const app = createApp(App);
app.use(myPlugin, { someOption: 'Hello' });
app.mount('#app');
Plugin Scope
Plugins can affect global or local scopes. Global plugins impact the entire application, while local plugins affect specific component trees.
// Global Plugin
app.use(myGlobalPlugin);
// Local Plugin
const childApp = app.createApp(ChildComponent);
childApp.use(myLocalPlugin);
Best Practices
- Modularity: Keep plugins small and focused on a single function for manageability and reuse.
- Documentation: Provide clear documentation on plugin installation and usage.
- Testing: Write unit tests to verify plugin behavior across environments.
- Compatibility: Ensure plugins work reliably across Vue versions and browsers.
Real-World Example: Custom Route Guard Plugin
Create a plugin to manage route guards, ensuring users are logged in before accessing certain pages.
// plugins/routerGuard.js
export default {
install(app) {
app.router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!app.config.globalProperties.$auth.isLoggedIn()) {
next({ name: 'login' });
} else {
next();
}
} else {
next();
}
});
}
};
Install and use the plugin in your application:
// main.js
import routerGuard from './plugins/routerGuard';
const app = createApp(App);
app.use(routerGuard);
app.use(router);
app.mount('#app');
This plugin checks user authentication before route navigation, redirecting unauthenticated users to the login page.
Code Analysis
- Component Basics: Modular, reusable components with props and events streamline development.
- Communication: Props, emits, and advanced patterns like Vuex/Pinia enable flexible data flow.
- Slots: Default, named, and scoped slots enhance component flexibility.
- Dynamic/Async Components: Optimize performance with lazy loading and runtime switching.
- Built-in Components:
<component>,<transition>,<keep-alive>,<slot>,<teleport>, and<Suspense>address common use cases. - Composables: Encapsulate reusable logic for maintainable, testable code.
- Custom Directives: Extend Vue’s functionality with reusable DOM behaviors.
- Plugins: Modular extensions enhance application functionality with global or local scope.



