Vue.js Interview Questions (Free Preview)
Free sample of 15 from 48 questions available
Basic Concepts
What is Vue.js and what are its primary purposes?
Vue.js is a progressive JavaScript framework for building user interfaces and single-page applications. It was created by Evan You and first released in 2014. Vue.js is designed to be incrementally adoptable, meaning you can integrate it into existing projects gradually or use it to build complete applications from scratch.
The primary purposes of Vue.js include:
- User Interface Development: Creating reactive and dynamic user interfaces with declarative rendering
- Component-Based Architecture: Building reusable, modular components that encapsulate functionality
- State Management: Managing application state through reactive data binding
- Single-Page Applications (SPAs): Developing fast, interactive web applications with client-side routing
- Progressive Enhancement: Allowing developers to adopt Vue gradually in existing projects
Vue.js follows the Model-View-ViewModel (MVVM) pattern and emphasizes simplicity, flexibility, and performance.
References:
↑ Back to topHow do data binding and interpolation work in Vue?
Data binding in Vue.js creates a connection between the component's data and the DOM, allowing automatic updates when data changes. Vue uses a reactive system to track dependencies and update the DOM efficiently.
Text Interpolation
Vue uses double curly braces (mustaches) for text interpolation:
<template>
<div>
<p>{{ message }}</p>
<p>{{ firstName + ' ' + lastName }}</p>
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
</div>
</template>
Raw HTML
Use v-html directive for raw HTML content:
<div v-html="rawHtml"></div>
Attribute Binding
Use v-bind directive or shorthand : for attribute binding:
<div v-bind:id="dynamicId"></div>
<div :id="dynamicId"></div>
<button :disabled="isButtonDisabled">Click me</button>
Two-Way Data Binding
Use v-model for two-way data binding on form inputs:
<input v-model="message" placeholder="Edit me">
<p>Message is: {{ message }}</p>
Reactivity System
Vue's reactivity system automatically tracks dependencies and updates the DOM when data changes:
export default {
data() {
return {
message: 'Hello Vue!',
firstName: 'John',
lastName: 'Doe'
}
}
}
References:
↑ Back to topWhat is the role of directives in Vue, and can you give examples of common directives?
Directives in Vue.js are special attributes with the v- prefix that provide declarative functionality to templates. They apply reactive behavior to the DOM when the value of their expression changes.
Common Built-in Directives:
v-if, v-else-if, v-else
Conditional rendering:
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Not A/B</div>
v-show
Toggle element visibility:
<div v-show="isVisible">This may be hidden</div>
v-for
List rendering:
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
v-model
Two-way data binding:
<input v-model="message">
<select v-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
v-bind (:)
Attribute binding:
<img :src="imageSrc" :alt="imageAlt">
<div :class="{ active: isActive, 'text-danger': hasError }">
v-on (@)
Event handling:
<button @click="handleClick">Click me</button>
<input @keyup.enter="submit">
v-html
Raw HTML insertion:
<div v-html="rawHtmlContent"></div>
v-text
Text content:
<span v-text="message"></span>
v-once
One-time interpolation:
<div v-once>{{ message }}</div>
Custom Directives
You can also create custom directives:
app.directive('focus', {
mounted(el) {
el.focus()
}
})
References:
↑ Back to topComponent Fundamentals
What lifecycle hooks are available in Vue components and why are they useful?
Vue components have several lifecycle hooks that allow you to tap into different stages of a component's lifecycle.
Vue 2/3 Options API Lifecycle Hooks
export default {
// Creation phase
beforeCreate() {
// Called before instance is created
// Data and events not yet available
console.log('beforeCreate')
},
created() {
// Called after instance is created
// Data, computed properties, methods available
// DOM not yet available
console.log('created')
this.fetchData() // Good place for API calls
},
// Mounting phase
beforeMount() {
// Called before initial render
// Template compiled but not yet rendered
console.log('beforeMount')
},
mounted() {
// Called after component is mounted to DOM
// DOM is available
console.log('mounted')
this.$refs.input.focus() // DOM manipulation
},
// Updating phase
beforeUpdate() {
// Called before component re-renders
console.log('beforeUpdate')
},
updated() {
// Called after component re-renders
console.log('updated')
// Be careful of infinite loops here
},
// Destruction phase
beforeUnmount() { // beforeDestroy in Vue 2
// Called before component is unmounted
console.log('beforeUnmount')
// Cleanup: remove event listeners, clear timers
clearInterval(this.timer)
},
unmounted() { // destroyed in Vue 2
// Called after component is unmounted
console.log('unmounted')
}
}
Vue 3 Composition API Lifecycle Hooks
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('beforeMount')
})
onMounted(() => {
console.log('mounted')
// DOM is available
})
onBeforeUpdate(() => {
console.log('beforeUpdate')
})
onUpdated(() => {
console.log('updated')
})
onBeforeUnmount(() => {
console.log('beforeUnmount')
// Cleanup
})
onUnmounted(() => {
console.log('unmounted')
})
}
}
Common Use Cases
Data Fetching
// Options API
created() {
this.fetchUserData()
},
// Composition API
onMounted(() => {
fetchUserData()
})
DOM Manipulation
// Options API
mounted() {
this.$refs.chart.initChart()
},
// Composition API
onMounted(() => {
chartRef.value.initChart()
})
Cleanup
// Options API
beforeUnmount() {
window.removeEventListener('scroll', this.handleScroll)
clearInterval(this.intervalId)
},
// Composition API
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
clearInterval(intervalId.value)
})
Lifecycle Diagram
┌─────────────────┐
│ beforeCreate │
└─────────────────┘
│
┌─────────────────┐
│ created │ ← Good for API calls
└─────────────────┘
│
┌─────────────────┐
│ beforeMount │
└─────────────────┘
│
┌─────────────────┐
│ mounted │ ← DOM available
└─────────────────┘
│
┌─────────────────┐
│ beforeUpdate │ ← Data changed
└─────────────────┘
│
┌─────────────────┐
│ updated │ ← DOM updated
└─────────────────┘
│
┌─────────────────┐
│ beforeUnmount │ ← Cleanup
└─────────────────┘
│
┌─────────────────┐
│ unmounted │
└─────────────────┘
References:
↑ Back to topHow can you communicate between parent and child components?
Vue provides several mechanisms for parent-child component communication, each serving different use cases.
1. Props (Parent → Child)
Basic Props
<!-- Parent Component -->
<template>
<child-component
:message="parentMessage"
:user="currentUser"
:is-active="true"
/>
</template>
<script>
export default {
data() {
return {
parentMessage: 'Hello from parent',
currentUser: { name: 'John', age: 30 }
}
}
}
</script>
<!-- Child Component -->
<template>
<div>
<p>{{ message }}</p>
<p>User: {{ user.name }}</p>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true
},
user: {
type: Object,
default: () => ({})
},
isActive: Boolean
}
}
</script>
2. Custom Events (Child → Parent)
Emitting Events
<!-- Child Component -->
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
emits: ['button-clicked', 'update-value'],
methods: {
handleClick() {
// Emit without data
this.$emit('button-clicked')
// Emit with data
this.$emit('update-value', { id: 1, value: 'new value' })
}
}
}
</script>
<!-- Parent Component -->
<template>
<child-component
@button-clicked="onButtonClicked"
@update-value="onValueUpdate"
/>
</template>
<script>
export default {
methods: {
onButtonClicked() {
console.log('Child button was clicked')
},
onValueUpdate(payload) {
console.log('Value updated:', payload)
}
}
}
</script>
3. v-model (Two-way Binding)
Custom v-model Implementation
<!-- Child Component (Input Component) -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
<!-- Parent Component -->
<template>
<custom-input v-model="inputValue" />
<p>Current value: {{ inputValue }}</p>
</template>
<script>
export default {
data() {
return {
inputValue: 'initial value'
}
}
}
</script>
4. Template Refs (Parent → Child Direct Access)
<!-- Parent Component -->
<template>
<child-component ref="childRef" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script>
export default {
methods: {
callChildMethod() {
// Direct access to child methods
this.$refs.childRef.childMethod()
// Access child data
console.log(this.$refs.childRef.childData)
}
}
}
</script>
<!-- Child Component -->
<script>
export default {
data() {
return {
childData: 'child data'
}
},
methods: {
childMethod() {
console.log('Child method called')
}
}
}
</script>
5. Provide/Inject (Ancestor → Descendant)
<!-- Grandparent Component -->
<script>
export default {
provide() {
return {
theme: 'dark',
userPermissions: this.permissions
}
},
data() {
return {
permissions: ['read', 'write']
}
}
}
</script>
<!-- Deep Child Component -->
<script>
export default {
inject: ['theme', 'userPermissions'],
created() {
console.log(this.theme) // 'dark'
console.log(this.userPermissions) // ['read', 'write']
}
}
</script>
6. Composition API Communication
// Parent Component
import { ref, provide } from 'vue'
export default {
setup() {
const sharedData = ref('shared value')
// Provide to descendants
provide('sharedData', sharedData)
const handleChildEvent = (data) => {
console.log('Received from child:', data)
}
return {
sharedData,
handleChildEvent
}
}
}
// Child Component
import { inject, emit } from 'vue'
export default {
emits: ['child-event'],
setup(props, { emit }) {
// Inject from ancestor
const sharedData = inject('sharedData')
const sendToParent = () => {
emit('child-event', { message: 'Hello parent' })
}
return {
sharedData,
sendToParent
}
}
}
Communication Patterns Summary
| Method | Direction | Use Case |
|---|---|---|
| Props | Parent → Child | Pass data down |
| Events | Child → Parent | Notify parent of changes |
| v-model | Bidirectional | Two-way data binding |
| Refs | Parent → Child | Direct method calls |
| Provide/Inject | Ancestor → Descendant | Deep component trees |
References:
↑ Back to topHow can slots be used to create flexible components?
Slots are Vue's content distribution mechanism that allows you to create flexible, reusable components by letting parent components pass content into predefined locations within child components.
Basic Slot Usage
Component with Default Slot
<!-- BaseCard.vue -->
<template>
<div class="card">
<div class="card-header">
<h3>Card Title</h3>
</div>
<div class="card-content">
<!-- Default slot - content goes here -->
<slot></slot>
</div>
</div>
</template>
Using the Component
<!-- Parent Component -->
<template>
<base-card>
<p>This content will be inserted into the slot</p>
<button>Action Button</button>
</base-card>
</template>
Named Slots
Component with Multiple Named Slots
<!-- BaseLayout.vue -->
<template>
<div class="layout">
<header class="header">
<slot name="header"></slot>
</header>
<aside class="sidebar">
<slot name="sidebar"></slot>
</aside>
<main class="main">
<!-- Default slot -->
<slot></slot>
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
Using Named Slots
<!-- Parent Component -->
<template>
<base-layout>
<template #header>
<h1>Page Title</h1>
<nav>Navigation items</nav>
</template>
<template #sidebar>
<ul>
<li>Sidebar item 1</li>
<li>Sidebar item 2</li>
</ul>
</template>
<!-- Default slot content -->
<p>This is the main content area</p>
<article>Main article content</article>
<template #footer>
<p>© 2024 My Company</p>
</template>
</base-layout>
</template>
Slot Fallback Content
Providing Default Content
<!-- BaseButton.vue -->
<template>
<button :class="buttonClass" @click="$emit('click')">
<slot>
<!-- Fallback content if no slot content provided -->
Click me
</slot>
</button>
</template>
<script>
export default {
props: {
variant: {
type: String,
default: 'primary'
}
},
computed: {
buttonClass() {
return `btn btn-${this.variant}`
}
}
}
</script>
Usage with and without slot content
<template>
<!-- Uses fallback content -->
<base-button variant="primary" @click="handleClick" />
<!-- Uses custom content -->
<base-button variant="secondary" @click="handleSave">
<icon name="save" /> Save Document
</base-button>
</template>
Dynamic Slot Names
<!-- TabComponent.vue -->
<template>
<div class="tabs">
<div class="tab-headers">
<button
v-for="tab in tabs"
:key="tab.name"
@click="activeTab = tab.name"
:class="{ active: activeTab === tab.name }"
>
{{ tab.label }}
</button>
</div>
<div class="tab-content">
<slot :name="activeTab"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
tabs: Array
},
data() {
return {
activeTab: this.tabs[0]?.name
}
}
}
</script>
Using Dynamic Slots
<template>
<tab-component :tabs="tabConfig">
<template #profile>
<user-profile :user="currentUser" />
</template>
<template #settings>
<user-settings @save="saveSettings" />
</template>
<template #notifications>
<notification-list :notifications="userNotifications" />
</template>
</tab-component>
</template>
Conditional Slots
Checking if Slot Content Exists
<!-- Modal.vue -->
<template>
<div class="modal" v-if="isVisible">
<div class="modal-content">
<!-- Header slot with conditional rendering -->
<div v-if="$slots.header" class="modal-header">
<slot name="header"></slot>
</div>
<!-- Default slot for body -->
<div class="modal-body">
<slot></slot>
</div>
<!-- Footer slot with fallback -->
<div class="modal-footer">
<slot name="footer">
<button @click="$emit('close')">Close</button>
</slot>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isVisible: Boolean
}
}
</script>
Renderless Components with Slots
Data Provider Component
<!-- DataProvider.vue -->
<template>
<div>
<slot
:data="data"
:loading="loading"
:error="error"
:refresh="fetchData"
></slot>
</div>
</template>
<script>
export default {
props: {
url: String
},
data() {
return {
data: null,
loading: false,
error: null
}
},
async created() {
await this.fetchData()
},
methods: {
async fetchData() {
this.loading = true
this.error = null
try {
const response = await fetch(this.url)
this.data = await response.json()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
}
}
}
</script>
Using Renderless Component
<template>
<data-provider url="/api/users" v-slot="{ data, loading, error, refresh }">
<div v-if="loading">Loading users...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<user-list :users="data" />
<button @click="refresh">Refresh</button>
</div>
</data-provider>
</template>
Advanced Slot Patterns
Slot Composition Pattern
<!-- Card.vue -->
<template>
<div class="card">
<slot name="image"></slot>
<div class="card-body">
<slot name="title"></slot>
<slot name="content"></slot>
<slot name="actions"></slot>
</div>
</div>
</template>
<!-- ProductCard.vue -->
<template>
<card>
<template #image>
<img :src="product.image" :alt="product.name" />
</template>
<template #title>
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
</template>
<template #content>
<p>{{ product.description }}</p>
</template>
<template #actions>
<button @click="addToCart">Add to Cart</button>
<button @click="addToWishlist">♥ Wishlist</button>
</template>
</card>
</template>
Composition API Slots
// Vue 3 Composition API
import { useSlots } from 'vue'
export default {
setup() {
const slots = useSlots()
// Check if slots exist
const hasHeader = !!slots.header
const hasFooter = !!slots.footer
return {
hasHeader,
hasFooter
}
}
}
Best Practices for Slots
- Use descriptive slot names that clearly indicate their purpose
- Provide fallback content for optional slots
- Document slot contracts - what data/props are expected
- Use conditional rendering to handle optional slots gracefully
- Keep slot APIs simple and focused
- Consider using scoped slots for data sharing
References:
↑ Back to topIntermediate Topics
What are computed properties and how do they differ from methods in Vue?
Computed properties are cached reactive values that are recalculated only when their dependencies change, while methods are functions that execute every time they are called.
Key Differences:
| Aspect | Computed Properties | Methods |
|---|---|---|
| Caching | Cached based on dependencies | No caching, executes on every call |
| Performance | Better for expensive operations | May cause unnecessary re-calculations |
| Reactivity | Automatically updates when dependencies change | Must be explicitly called |
| Usage | For derived data that depends on reactive data | For event handlers and actions |
Example:
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe',
items: [1, 2, 3, 4, 5]
}
},
computed: {
// Cached - only recalculates when firstName or lastName changes
fullName() {
console.log('Computing fullName')
return `${this.firstName} ${this.lastName}`
},
expensiveCalculation() {
return this.items.reduce((sum, item) => sum + item * Math.random(), 0)
}
},
methods: {
// Executes every time it's called
getFullName() {
console.log('Calling getFullName method')
return `${this.firstName} ${this.lastName}`
}
}
}
Reference: Vue.js Official Documentation - Computed Properties
↑ Back to topHow does Vue's reactivity system work, and what are some potential pitfalls with reactivity?
Vue's reactivity system automatically tracks dependencies and updates the UI when reactive data changes. It uses getters/setters (Vue 2) or Proxies (Vue 3) to detect changes.
How Vue 2 Reactivity Works:
- Object.defineProperty() converts properties to getters/setters
- Dependency Tracking - During render, Vue tracks which properties are accessed
- Change Detection - When a setter is triggered, Vue knows which components to re-render
- Virtual DOM Diffing - Vue calculates minimal changes needed
How Vue 3 Reactivity Works:
- Proxy Objects intercept property access and modifications
- Effect System tracks reactive dependencies automatically
- Ref/Reactive APIs provide explicit reactivity control
Vue 3 Example:
import { reactive, ref, computed, watchEffect } from 'vue'
export default {
setup() {
// Reactive object
const state = reactive({
count: 0,
user: {
name: 'John',
email: 'john@example.com'
}
})
// Reactive primitive
const message = ref('Hello')
// Computed property
const doubledCount = computed(() => state.count * 2)
// Watcher
watchEffect(() => {
console.log(`Count is: ${state.count}`)
})
return {
state,
message,
doubledCount
}
}
}
Common Reactivity Pitfalls:
1. Vue 2: Array Index and Length Issues
// ❌ Won't trigger reactivity in Vue 2
this.items[0] = newValue
this.items.length = 0
// ✅ Correct way in Vue 2
this.$set(this.items, 0, newValue)
this.items.splice(0)
2. Vue 2: Adding New Object Properties
// ❌ Won't be reactive in Vue 2
this.user.newProperty = 'value'
// ✅ Correct way in Vue 2
this.$set(this.user, 'newProperty', 'value')
// or
this.user = { ...this.user, newProperty: 'value' }
3. Losing Reactivity by Destructuring
// ❌ Loses reactivity
const { count } = this.state
count++ // Won't update UI
// ✅ Keep reactivity
this.state.count++
4. Async Updates and nextTick
// Vue updates are asynchronous
this.message = 'Updated'
console.log(this.$el.textContent) // Still shows old value
// ✅ Use nextTick
this.message = 'Updated'
this.$nextTick(() => {
console.log(this.$el.textContent) // Shows updated value
})
5. Memory Leaks with Watchers
// ❌ Manual watcher not cleaned up
const unwatch = this.$watch('someData', callback)
// Should call unwatch() in beforeDestroy
// ✅ Component watchers are automatically cleaned up
watch: {
someData(newVal) {
// Automatically cleaned up
}
}
Performance Considerations:
- Deep Watching can be expensive for large objects
- Avoid Mutating Props directly
- Use Object.freeze() for static data to prevent unnecessary reactivity
References:
↑ Back to topHow would you optimize a Vue application for performance?
Vue application performance optimization involves multiple strategies across different areas: bundle size, runtime performance, memory usage, and network efficiency.
1. Bundle Optimization
Code Splitting and Lazy Loading
// Route-based code splitting
const Home = () => import('@/views/Home.vue')
const About = () => import('@/views/About.vue')
const UserProfile = () => import(
/* webpackChunkName: "user" */ '@/views/UserProfile.vue'
)
const router = new VueRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/profile', component: UserProfile }
]
})
Dynamic Component Loading
<template>
<div>
<!-- Load heavy component only when needed -->
<component
:is="currentComponent"
v-if="showHeavyComponent"
/>
<!-- Async component with loading state -->
<AsyncComponent />
</div>
</template>
<script>
const AsyncComponent = () => ({
component: import('@/components/HeavyComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 3000
})
export default {
components: {
AsyncComponent
}
}
</script>
Tree Shaking and Dead Code Elimination
// ❌ Imports entire lodash library
import _ from 'lodash'
// ✅ Import only needed functions
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'
// ✅ Use ES6 modules for better tree shaking
import { debounce, throttle } from 'lodash-es'
2. Runtime Performance Optimization
Use v-show vs v-if Appropriately
<template>
<!-- Use v-if for conditionally rendered content -->
<UserSettings v-if="user.isLoggedIn" />
<!-- Use v-show for frequently toggled content -->
<Modal v-show="isModalVisible" />
</template>
Optimize List Rendering
<template>
<!-- Always use :key for list items -->
<div
v-for="item in items"
:key="item.id"
class="list-item"
>
{{ item.name }}
</div>
<!-- Use v-memo for expensive list items (Vue 3) -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id, item.selected]"
>
<ExpensiveChild :data="item" />
</div>
</template>
Computed Properties vs Methods
export default {
computed: {
// ✅ Cached, only recalculates when dependencies change
expensiveValue() {
return this.items.filter(item => item.active)
.reduce((sum, item) => sum + item.value, 0)
}
},
methods: {
// ❌ Executes on every render
getExpensiveValue() {
return this.items.filter(item => item.active)
.reduce((sum, item) => sum + item.value, 0)
}
}
}
Use Object.freeze() for Static Data
export default {
data() {
return {
// Prevent Vue from making this reactive
staticData: Object.freeze({
categories: ['A', 'B', 'C'],
constants: { MAX_SIZE: 100, MIN_SIZE: 10 }
}),
// Large datasets that don't change
chartData: Object.freeze(largeDataset)
}
}
}
3. Memory Management
Proper Event Listener Cleanup
export default {
mounted() {
window.addEventListener('resize', this.handleResize)
this.intervalId = setInterval(this.updateData, 1000)
},
beforeDestroy() {
// ✅ Clean up to prevent memory leaks
window.removeEventListener('resize', this.handleResize)
clearInterval(this.intervalId)
},
methods: {
handleResize() {
// Handle window resize
}
}
}
Use WeakMap for Private Data
const privateData = new WeakMap()
export default {
created() {
// Store private data that gets garbage collected
privateData.set(this, {
sensitiveInfo: 'secret',
temporaryCache: new Map()
})
}
}
4. Asset Optimization
Image Optimization
<template>
<!-- Use modern image formats -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.avif" type="image/avif">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
<!-- Responsive images -->
<img
:src="imageSrc"
:srcset="imageSrcSet"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="lazy"
alt="Responsive image"
>
</template>
Font Optimization
/* Preload critical fonts */
@font-face {
font-family: 'MyCustomFont';
src: url('./fonts/font.woff2') format('woff2');
font-display: swap; /* Improve perceived performance */
}
5. Virtual Scrolling for Large Lists
<template>
<!-- Use virtual scrolling for large datasets -->
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
<script>
import { RecycleScroller } from 'vue-virtual-scroller'
export default {
components: {
RecycleScroller
}
}
</script>
6. State Management Optimization
Normalize State Structure
// ❌ Nested, hard to update
state: {
users: [
{ id: 1, name: 'John', posts: [{ id: 1, title: 'Post 1' }] }
]
}
// ✅ Normalized, easier to update
state: {
users: {
byId: {
1: { id: 1, name: 'John', postIds: [1] }
},
allIds: [1]
},
posts: {
byId: {
1: { id: 1, title: 'Post 1', userId: 1 }
},
allIds: [1]
}
}
Use Vuex Modules
// Store modules for better performance
const userModule = {
namespaced: true,
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
user: userModule,
products: productModule
}
})
7. Network Optimization
API Request Optimization
// Debounce search requests
import { debounce } from 'lodash-es'
export default {
data() {
return {
searchQuery: '',
searchResults: []
}
},
watch: {
searchQuery: debounce(function(newQuery) {
if (newQuery.length > 2) {
this.performSearch(newQuery)
}
}, 300)
},
methods: {
async performSearch(query) {
// Cancel previous request if still pending
if (this.searchController) {
this.searchController.abort()
}
this.searchController = new AbortController()
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: this.searchController.signal
})
this.searchResults = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search failed:', error)
}
}
}
}
}
8. Build Configuration Optimization
// vue.config.js
const path = require('path')
module.exports = {
// Production optimizations
productionSourceMap: false,
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// Optimize chunks
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
},
// Enable modern mode
modern: true,
// PWA optimizations
pwa: {
workboxOptions: {
runtimeCaching: [{
urlPattern: /^https:\/\/api\./,
handler: 'CacheFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 300
}
}
}]
}
}
}
Performance Monitoring
// Performance monitoring
export default {
async mounted() {
// Measure component mount time
const startTime = performance.now()
await this.loadData()
const endTime = performance.now()
console.log(`Component loaded in ${endTime - startTime}ms`)
// Use Performance Observer API
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration)
}
})
observer.observe({ entryTypes: ['measure', 'navigation'] })
}
}
}
Performance Measurement Tools:
- Vue DevTools - Component performance profiling
- Lighthouse - Web performance auditing
- webpack-bundle-analyzer - Bundle size analysis
- Chrome DevTools - Runtime performance profiling
References:
↑ Back to topWhat are single file components (SFCs) and what advantages do they provide?
Single File Components (SFCs) are Vue.js files with a .vue extension that encapsulate the template, script, and style of a component in a single file. They provide a cohesive way to organize component-related code.
SFC Structure:
<template>
<div class="my-component">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<button @click="handleClick">{{ buttonText }}</button>
</div>
</template>
<script>
export default {
name: 'MyComponent',
props: {
title: {
type: String,
required: true
},
description: String
},
data() {
return {
buttonText: 'Click me!'
}
},
methods: {
handleClick() {
this.$emit('button-clicked', this.title)
}
}
}
</script>
<style scoped>
.my-component {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
.my-component h1 {
color: #42b883;
margin-bottom: 10px;
}
.my-component button {
background-color: #42b883;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.my-component button:hover {
background-color: #369870;
}
</style>
Key Advantages:
1. Co-location of Concerns
All component-related code is in one file, making it easier to understand and maintain:
<!-- UserCard.vue -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" class="avatar">
<div class="user-info">
<h3 class="user-name">{{ user.name }}</h3>
<p class="user-email">{{ user.email }}</p>
<span class="user-status" :class="statusClass">
{{ user.status }}
</span>
</div>
</div>
</template>
<script>
export default {
name: 'UserCard',
props: ['user'],
computed: {
statusClass() {
return {
'status-online': this.user.status === 'online',
'status-offline': this.user.status === 'offline',
'status-away': this.user.status === 'away'
}
}
}
}
</script>
<style scoped>
.user-card {
display: flex;
padding: 15px;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
}
.user-info {
flex: 1;
}
.user-name {
margin: 0 0 5px 0;
font-size: 16px;
font-weight: 600;
}
.user-email {
margin: 0 0 8px 0;
color: #666;
font-size: 14px;
}
.user-status {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
text-transform: uppercase;
}
.status-online { background: #4CAF50; color: white; }
.status-offline { background: #9E9E9E; color: white; }
.status-away { background: #FF9800; color: white; }
</style>
2. Scoped CSS
CSS styles are automatically scoped to the component, preventing style conflicts:
<template>
<div class="button">
<span class="text">{{ label }}</span>
</div>
</template>
<style scoped>
/* These styles only apply to this component */
.button {
background: blue;
color: white;
padding: 10px;
}
.text {
font-weight: bold;
}
/* Deep selectors for child components */
.button :deep(.child-class) {
color: red;
}
/* Global selectors */
:global(.global-class) {
margin: 0;
}
</style>
Generated CSS with unique scope identifiers:
.button[data-v-f3f3eg9] {
background: blue;
color: white;
padding: 10px;
}
.text[data-v-f3f3eg9] {
font-weight: bold;
}
3. CSS Pre-processor Support
SFCs support various CSS pre-processors out of the box:
<style lang="scss" scoped>
$primary-color: #42b883;
$secondary-color: #35495e;
.component {
background: $primary-color;
&:hover {
background: darken($primary-color, 10%);
}
.nested {
color: $secondary-color;
@include respond-to(mobile) {
font-size: 14px;
}
}
}
</style>
<style lang="stylus" scoped>
$primary = #42b883
.component
background $primary
&:hover
background darken($primary, 10%)
</style>
4. Hot Module Replacement (HMR)
SFCs support efficient hot reloading during development:
- Template changes → Re-render component in place
- Script changes → Re-create component instances
- Style changes → Update styles without full reload
5. Build-time Optimizations
SFCs enable various build-time optimizations:
<template>
<!-- Build-time template compilation -->
<div>
<h1>{{ title }}</h1>
<!-- Static hoisting for performance -->
<div class="static-content">This won't change</div>
</div>
</template>
<script>
// Tree-shaking friendly imports
import { computed, ref } from 'vue'
import { debounce } from 'lodash-es'
export default {
setup() {
const title = ref('Hello')
// Build-time dead code elimination
const unused = computed(() => 'This will be removed if unused')
return { title }
}
}
</script>
<style scoped>
/* Unused styles are automatically removed */
.used-class {
color: red;
}
.unused-class {
color: blue; /* Removed in production build */
}
</style>
6. TypeScript Support
SFCs have excellent TypeScript integration:
<template>
<div>
<h1>{{ title }}</h1>
<button @click="increment">Count: {{ count }}</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
export default defineComponent({
name: 'TypedComponent',
props: {
user: {
type: Object as () => User,
required: true
}
},
setup(props) {
const count = ref<number>(0)
const title = computed<string>(() =>
`Hello, ${props.user.name}!`
)
const increment = (): void => {
count.value++
}
return {
count,
title,
increment
}
}
})
</script>
7. Testing Benefits
SFCs are easier to test as complete units:
// Component.spec.js
import { shallowMount } from '@vue/test-utils'
import UserCard from '@/components/UserCard.vue'
describe('UserCard.vue', () => {
const mockUser = {
name: 'John Doe',
email: 'john@example.com',
avatar: 'avatar.jpg',
status: 'online'
}
it('renders user information correctly', () => {
const wrapper = shallowMount(UserCard, {
props: { user: mockUser }
})
expect(wrapper.find('.user-name').text()).toBe('John Doe')
expect(wrapper.find('.user-email').text()).toBe('john@example.com')
expect(wrapper.find('.status-online').exists()).toBe(true)
})
it('applies correct status class', () => {
const wrapper = shallowMount(UserCard, {
props: {
user: { ...mockUser, status: 'offline' }
}
})
expect(wrapper.find('.status-offline').exists()).toBe(true)
})
})
8. IDE Support
Modern IDEs provide excellent support for SFCs:
- Syntax Highlighting for template, script, and style blocks
- IntelliSense for Vue APIs and component props
- Error Detection and type checking
- Auto-completion for directives and component names
- Refactoring support across template and script sections
9. CSS Modules Support
<template>
<div :class="$style.wrapper">
<h1 :class="$style.title">{{ title }}</h1>
</div>
</template>
<style module>
.wrapper {
padding: 20px;
}
.title {
color: #42b883;
}
</style>
10. Multiple Style Blocks
<template>
<div class="component">
<h1>Title</h1>
</div>
</template>
<!-- Global styles -->
<style>
body {
font-family: Arial, sans-serif;
}
</style>
<!-- Scoped styles -->
<style scoped>
.component {
padding: 20px;
}
</style>
<!-- CSS modules -->
<style module="classes">
.special {
background: yellow;
}
</style>
Comparison with Traditional Approach:
| Aspect | SFCs | Traditional Separation |
|---|---|---|
| Organization | Co-located in one file | Separate HTML/CSS/JS files |
| Maintenance | Easier to modify related code | Need to navigate multiple files |
| Scoping | Automatic CSS scoping | Manual class naming conventions |
| Hot Reload | Granular updates | Full page reload |
| Build Tools | Integrated processing | Manual configuration |
| Type Safety | Full TypeScript support | Limited cross-file checking |
Build Process: SFCs are processed by the Vue compiler during build time:
- Template → Render function
- Script → Component options/setup function
- Style → Scoped CSS with unique identifiers
- Optimization → Dead code elimination, tree shaking
References:
↑ Back to topState Management
What is Vuex and why would you use it in a project?
Vuex is the official state management library for Vue.js applications. It serves as a centralized store for all components in an application, implementing a predictable state container pattern similar to Redux in React or Flux architecture.
Why use Vuex?
Centralized State Management: Vuex provides a single source of truth for your application's state, making it easier to track and debug state changes across complex applications.
Predictable State Changes: All state mutations happen through well-defined channels (mutations), making the application's behavior more predictable and easier to reason about.
Component Communication: Eliminates the need for complex prop drilling or event bubbling between deeply nested components.
Time-Travel Debugging: Integration with Vue DevTools allows you to inspect state changes and even revert to previous states.
SSR Support: Works seamlessly with server-side rendering applications.
Vuex is particularly beneficial in medium to large-scale applications where multiple components need to share and modify the same state.
References:
↑ Back to topRouting and Navigation
What is Vue Router and why is it important in single-page applications?
Vue Router is the official routing library for Vue.js applications. It provides a declarative way to handle client-side routing in single-page applications (SPAs).
Key characteristics of Vue Router:
- Client-side routing: Manages navigation between different views without full page reloads
- URL synchronization: Keeps the browser URL in sync with the application state
- History management: Provides browser history support with back/forward buttons
- Component-based: Routes are mapped to Vue components
Importance in SPAs:
- User Experience: Enables fast navigation without page refreshes, creating a smooth, app-like experience
- SEO Benefits: Supports HTML5 history mode for clean URLs and better search engine indexing
- State Management: Maintains application state during navigation
- Deep Linking: Allows users to bookmark and share specific application states via URLs
// Basic Vue Router setup
import { createRouter, createWebHistory } from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
const router = createRouter({
history: createWebHistory(),
routes
})
References:
↑ Back to topWhat are navigation guards and how do they help in controlling access to routes?
Navigation guards are functions that allow you to control navigation flow in Vue Router applications. They act as middleware that can prevent, redirect, or modify navigation attempts.
Types of Navigation Guards:
1. Global Guards
// Global before guard
router.beforeEach((to, from, next) => {
// Authentication check
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated()) {
next('/login')
return
}
}
next()
})
// Global resolve guard
router.beforeResolve((to, from, next) => {
// Called after all component guards and async route components are resolved
next()
})
// Global after hook
router.afterEach((to, from) => {
// Analytics tracking, page titles, etc.
document.title = to.meta.title || 'Default Title'
})
2. Per-Route Guards
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
if (!hasAdminRole()) {
next('/unauthorized')
return
}
next()
}
}
]
3. In-Component Guards
export default {
beforeRouteEnter(to, from, next) {
// Called before route is confirmed
// No access to `this` component instance
next(vm => {
// Access to component instance via `vm`
})
},
beforeRouteUpdate(to, from, next) {
// Called when route changes but component is reused
// Has access to `this`
this.fetchData(to.params.id)
next()
},
beforeRouteLeave(to, from, next) {
// Called when navigating away from current route
if (this.hasUnsavedChanges) {
const answer = window.confirm('You have unsaved changes. Leave anyway?')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
}
}
Common Use Cases:
- Authentication & Authorization:
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const currentUser = getCurrentUser()
if (requiresAuth && !currentUser) {
next('/login')
} else if (to.path === '/login' && currentUser) {
next('/')
} else {
next()
}
})
- Data Fetching:
beforeRouteEnter(to, from, next) {
fetchUserData(to.params.id).then(user => {
next(vm => vm.setUser(user))
}).catch(() => next('/404'))
}
- Form Validation:
beforeRouteLeave(to, from, next) {
if (this.formDirty && !window.confirm('Discard changes?')) {
next(false)
} else {
next()
}
}
Navigation Guard Flow:
- Navigation triggered
- Call leave guards in deactivated components
- Call global
beforeEachguards - Call
beforeRouteUpdateguards in reused components - Call
beforeEnterin route configs - Resolve async route components
- Call
beforeRouteEnterin activated components - Call global
beforeResolveguards - Navigation confirmed
- Call global
afterEachhooks
References:
↑ Back to topComposition API and Advanced Patterns
What is the Composition API and how does it differ from the Options API?
The Composition API is a new way to organize and reuse component logic in Vue 3, introduced as an alternative to the traditional Options API. It provides a more flexible and powerful approach to building Vue components.
Key Differences:
| Options API | Composition API |
|---|---|
| Logic organized by option types (data, methods, computed, etc.) | Logic organized by feature/concern |
Uses this context |
Uses explicit imports and function calls |
| Less flexible for code reuse | Highly composable and reusable |
| Better for beginners | More powerful for complex applications |
Options API Example:
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
}
}
Composition API Example:
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return {
count,
message,
doubleCount,
increment
}
}
}
References:
↑ Back to topCan you explain how to reuse code across components with Composition API's custom hooks (composables)?
Composables are reusable functions that encapsulate stateful logic using the Composition API. They follow the naming convention of starting with "use" (e.g., useCounter, useAuth).
Basic Composable Structure
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const isEven = computed(() => count.value % 2 === 0)
const isPositive = computed(() => count.value > 0)
return {
// State
count: readonly(count), // Make it readonly if needed
// Computed
isEven,
isPositive,
// Actions
increment,
decrement,
reset
}
}
Using Composables in Components
// components/CounterComponent.vue
import { useCounter } from '@/composables/useCounter'
export default {
setup() {
const { count, isEven, increment, decrement, reset } = useCounter(10)
return {
count,
isEven,
increment,
decrement,
reset
}
}
}
Advanced Composables Examples
1. API Fetching Composable
// composables/useFetch.js
import { ref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url.value || url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// Auto-fetch when URL changes
watchEffect(() => {
if (url) {
fetchData()
}
})
return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
refetch: fetchData
}
}
// Usage
export default {
setup() {
const userId = ref(1)
const userUrl = computed(() => `/api/users/${userId.value}`)
const { data: user, loading, error, refetch } = useFetch(userUrl)
return { user, loading, error, refetch, userId }
}
}
2. Local Storage Composable
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue
const value = ref(initialValue)
// Watch for changes and update localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
// Method to remove from localStorage
const remove = () => {
localStorage.removeItem(key)
value.value = defaultValue
}
return {
value,
remove
}
}
// Usage
export default {
setup() {
const { value: theme, remove: clearTheme } = useLocalStorage('theme', 'light')
const { value: userPrefs } = useLocalStorage('userPreferences', {
language: 'en',
notifications: true
})
return { theme, clearTheme, userPrefs }
}
}
3. Form Validation Composable
// composables/useForm.js
import { ref, reactive, computed } from 'vue'
export function useForm(initialValues = {}, validationRules = {}) {
const values = reactive({ ...initialValues })
const errors = reactive({})
const touched = reactive({})
const validate = (field) => {
const rule = validationRules[field]
if (rule) {
const error = rule(values[field])
if (error) {
errors[field] = error
} else {
delete errors[field]
}
}
}
const validateAll = () => {
Object.keys(validationRules).forEach(validate)
}
const handleChange = (field, value) => {
values[field] = value
touched[field] = true
validate(field)
}
const isValid = computed(() => Object.keys(errors).length === 0)
const hasErrors = computed(() => Object.keys(errors).length > 0)
const reset = () => {
Object.assign(values, initialValues)
Object.keys(errors).forEach(key => delete errors[key])
Object.keys(touched).forEach(key => delete touched[key])
}
return {
values,
errors,
touched,
isValid,
hasErrors,
handleChange,
validate,
validateAll,
reset
}
}
// Usage
export default {
setup() {
const { values, errors, isValid, handleChange, validateAll } = useForm(
{ email: '', password: '' },
{
email: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid'
return null
},
password: (value) => {
if (!value) return 'Password is required'
if (value.length < 6) return 'Password must be at least 6 characters'
return null
}
}
)
const submit = () => {
validateAll()
if (isValid.value) {
// Submit form
console.log('Form submitted:', values)
}
}
return {
values,
errors,
isValid,
handleChange,
submit
}
}
}
4. Mouse Position Composable
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
const updateMouse = (event) => {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updateMouse)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updateMouse)
})
return { x: readonly(x), y: readonly(y) }
}
Composable Composition Pattern
// composables/useUserProfile.js
import { computed } from 'vue'
import { useFetch } from './useFetch'
import { useLocalStorage } from './useLocalStorage'
export function useUserProfile(userId) {
// Compose multiple composables
const { data: user, loading, error } = useFetch(`/api/users/${userId}`)
const { value: cachedPreferences } = useLocalStorage('userPrefs', {})
const fullName = computed(() => {
return user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
})
const preferences = computed(() => {
return { ...cachedPreferences.value, ...user.value?.preferences }
})
return {
user,
loading,
error,
fullName,
preferences
}
}
Best Practices for Composables
- Naming Convention: Always start with "use"
- Return Consistent Objects: Return an object with descriptive property names
- Use
readonly(): For state that shouldn't be mutated externally - Accept Parameters: Make composables configurable
- Handle Cleanup: Use lifecycle hooks for cleanup when necessary
- Compose Together: Combine multiple composables for complex functionality
References:
↑ Back to top