Vue.js holds an interesting position in the framework ecosystem. While React dominates in raw job postings and Angular commands enterprise presence, Vue has carved out a devoted following among developers who value its gentle learning curve and elegant API design. Companies like GitLab, Nintendo, and Adobe use Vue extensively.
What makes Vue interviews distinctive is the framework's dual personality: you might be asked about the Options API patterns that made Vue famous, or the newer Composition API that brings it closer to React's hooks model. This guide covers the Vue.js questions that actually appear in technical interviews.
Table of Contents
- Vue.js Fundamentals Questions
- Composition API Questions
- Reactivity System Questions
- Computed Properties and Watchers Questions
- Component Lifecycle Questions
- State Management Questions
- Slots and Component Design Questions
- Forms and Validation Questions
- Vue Router Questions
- Performance and Testing Questions
Vue.js Fundamentals Questions
Understanding Vue's core philosophy and architecture is essential for any interview.
What is Vue.js and what makes it different from other frameworks?
Vue is a progressive JavaScript framework for building user interfaces. Unlike React's JSX-everywhere approach or Angular's full framework commitment, Vue lets you adopt features incrementally—start with just the view layer and add routing, state management, and build tooling as needed.
Vue 3 introduced the Composition API alongside the existing Options API, giving developers two paradigms for organizing component logic. The reactivity system uses JavaScript Proxies to automatically track dependencies and update the DOM efficiently. State management moved from Vuex to Pinia, which offers a simpler API with full TypeScript support.
What is the difference between v-if and v-show?
Both directives control element visibility, but they work differently under the hood. The v-if directive completely removes and recreates the element from the DOM based on the condition. The v-show directive always keeps the element in the DOM but toggles its CSS display property.
Use v-if when the condition rarely changes or when you have expensive components that shouldn't render at all. Use v-show when you need frequent toggling, as the initial render cost is higher but subsequent toggles are cheaper.
Why do you need :key in v-for loops?
The key attribute helps Vue's virtual DOM algorithm identify which items have changed, been added, or been removed in a list. Without keys, Vue uses an "in-place patch" strategy that reuses elements and may cause subtle bugs with component state or animations.
When you provide unique keys, Vue can track each node's identity and reorder, add, or remove elements correctly. Always use stable, unique identifiers (like database IDs) rather than array indices as keys.
Composition API Questions
The Composition API is Vue 3's most significant addition and a common interview topic.
What is the Composition API and how does it differ from the Options API?
The Composition API is Vue 3's alternative to the Options API for organizing component logic. The key philosophical difference is how you structure code: Options API organizes by type (all data together, all methods together), while Composition API organizes by feature (all code for a feature in one place).
Consider a component that manages a counter and also fetches user data:
// Options API - logic scattered by type
export default {
data() {
return {
count: 0,
user: null,
loading: false,
error: null
}
},
computed: {
doubleCount() {
return this.count * 2
},
userDisplayName() {
return this.user?.name || 'Anonymous'
}
},
methods: {
increment() {
this.count++
},
async fetchUser(id) {
this.loading = true
try {
const response = await fetch(`/api/users/${id}`)
this.user = await response.json()
} catch (e) {
this.error = e.message
} finally {
this.loading = false
}
}
},
mounted() {
this.fetchUser(this.$route.params.id)
}
}The counter logic (count, doubleCount, increment) and the user logic (user, loading, error, fetchUser, userDisplayName) are interleaved throughout the component. Now compare the Composition API approach:
// Composition API - logic grouped by feature
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
export default {
setup() {
// Counter feature - all together
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
// User feature - all together
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const userDisplayName = computed(() => user.value?.name || 'Anonymous')
async function fetchUser(id) {
loading.value = true
try {
const response = await fetch(`/api/users/${id}`)
user.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
const route = useRoute()
onMounted(() => fetchUser(route.params.id))
return {
count,
doubleCount,
increment,
user,
loading,
error,
userDisplayName
}
}
}What are composables and why are they better than mixins?
Composables are functions that use Vue's Composition API to encapsulate and reuse stateful logic. They solve the problems that mixins had: implicit dependencies, naming conflicts, and unclear data sources.
The user-fetching logic from the previous example can be extracted into a reusable composable:
// composables/useUser.js
import { ref, computed } from 'vue'
export function useUser() {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const displayName = computed(() => user.value?.name || 'Anonymous')
async function fetchUser(id) {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${id}`)
user.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
return { user, loading, error, displayName, fetchUser }
}Any component can now use this with a single line: const { user, loading, fetchUser } = useUser(). With mixins, you'd hit naming conflicts and lose track of where properties come from.
When would you still use the Options API?
The Options API remains valuable for simpler components where its structure aids readability, for teams transitioning gradually from Vue 2, or for cases where the component is unlikely to need logic extraction. The Options API's explicit structure—data here, methods there—provides clear organization for straightforward components.
Reactivity System Questions
Understanding Vue's reactivity is crucial for debugging and optimization.
How does Vue's reactivity system work in Vue 3?
Vue's reactivity system automatically tracks which data a component uses and triggers re-renders when that data changes. Vue 3 implements this using JavaScript Proxies, which is a significant improvement over Vue 2's Object.defineProperty approach.
When you wrap data with reactive() or ref(), Vue creates a Proxy around it. When your component renders, Vue tracks which properties you access—these become dependencies. When any dependency changes, Vue knows exactly which components need to re-render.
import { reactive, ref, effect, computed } from 'vue'
// When you create reactive state
const state = reactive({
count: 0,
user: { name: 'John' }
})
// Vue wraps it in a Proxy that intercepts gets and sets
// Conceptually, something like this happens internally:
const handler = {
get(target, key) {
track(target, key) // "Someone is reading 'count'"
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // "count changed, notify watchers"
return true
}
}
// ref() works similarly but wraps the value in an object with .value
const count = ref(0)
// Internally: { value: 0 } wrapped in a Proxy
// Access via count.value triggers trackingWhat reactivity limitations did Vue 2 have that Vue 3 fixed?
Vue 3's Proxy-based system fixed several Vue 2 limitations. In Vue 2, you needed $set or Vue.set for array index assignment, length modification, and adding new properties. Vue 3 handles all of these automatically:
// Vue 2 couldn't detect these (needed $set or Vue.set)
const state = reactive({ items: [1, 2, 3] })
// All of these work automatically in Vue 3
state.items[0] = 'new value' // Array index assignment
state.items.length = 0 // Length modification
state.newProperty = 'hello' // Adding new properties
// Vue 2 required workarounds:
// this.$set(this.items, 0, 'new value')
// this.$set(this, 'newProperty', 'hello')What are the common reactivity gotchas in Vue 3?
Even with the improved Proxy-based system, there are patterns that can break reactivity:
// GOTCHA 1: Destructuring loses reactivity
const { count } = state // count is now just a number, not reactive
count++ // Won't trigger updates!
// Solution: use toRefs or access directly
const { count } = toRefs(state) // count.value is reactive
// or just: state.count++
// GOTCHA 2: Replacing entire reactive object
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // Components watching original state won't update
// Solution: mutate properties, don't replace the object
state.count = 1 // Works correctly
// GOTCHA 3: ref unwrapping in reactive objects
const count = ref(0)
const state = reactive({ count }) // count is auto-unwrapped
state.count++ // Works, no .value needed
count.value // Still need .value when accessing ref directly
// GOTCHA 4: Async timing
const message = ref('hello')
message.value = 'world'
console.log(document.body.textContent) // Still shows 'hello'
// DOM updates are batched and asynchronous
import { nextTick } from 'vue'
await nextTick()
console.log(document.body.textContent) // Now shows 'world'What is the difference between ref and reactive?
The ref() function wraps any value (including primitives) in an object with a .value property. The reactive() function creates a proxy directly on an object without the .value wrapper.
Use ref() for primitives and when you might reassign the whole value. Use reactive() when you have an object whose properties you'll modify but won't replace entirely.
Computed Properties and Watchers Questions
These questions test understanding of Vue's caching mechanisms and side effect handling.
What is the difference between computed properties and methods?
Computed properties and methods both let you derive values from your state, but they serve different purposes. The key difference is caching: computed properties cache their results based on their reactive dependencies, while methods execute fresh on every call.
import { ref, computed } from 'vue'
export default {
setup() {
const items = ref([
{ name: 'Product A', price: 100, quantity: 2 },
{ name: 'Product B', price: 50, quantity: 3 },
{ name: 'Product C', price: 75, quantity: 1 }
])
// Computed: cached until items changes
const totalPrice = computed(() => {
console.log('Computing total price...')
return items.value.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
})
// Method: runs every single time it's called
function calculateTotal() {
console.log('Calculating total...')
return items.value.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
}
return { items, totalPrice, calculateTotal }
}
}In the template, if you use {{ totalPrice }} three times, you'll see "Computing total price..." logged only once. Use {{ calculateTotal() }} three times, and you'll see it logged three times.
When should you use watch vs watchEffect?
The watch function explicitly specifies what to watch and runs only when those specific values change. The watchEffect function automatically tracks any reactive dependencies used inside it and runs immediately on setup.
Use watch when you need to know the previous value, when you want lazy execution, or when you need precise control over what triggers the callback. Use watchEffect for simpler cases where you just want to run side effects whenever any dependency changes.
Why should computed properties be side-effect free?
Computed properties are for deriving data, not for side effects. Vue may skip recomputing if dependencies haven't changed, and the computation order isn't guaranteed. Side effects in computed properties lead to unpredictable behavior.
// WRONG: Computed with side effects
const filteredUsers = computed(() => {
analytics.track('filtered users') // Side effect!
return users.value.filter(u => u.active)
})
// RIGHT: Use watch for side effects
watch(
() => users.value.filter(u => u.active),
(filtered) => {
analytics.track('filtered users', { count: filtered.length })
}
)Component Lifecycle Questions
Lifecycle hooks questions test understanding of component behavior at different stages.
What are Vue's component lifecycle hooks and when do they run?
Vue components go through several distinct phases, each with hooks that let you run code at specific moments. Understanding these isn't just about knowing the names—it's about knowing which hook is appropriate for different tasks.
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
ref
} from 'vue'
export default {
setup() {
const data = ref(null)
const chartRef = ref(null)
let resizeObserver = null
// BEFORE MOUNT: Reactive setup is done, but no DOM yet
// Good for: Initial data fetching that doesn't need DOM
onBeforeMount(() => {
console.log('Component will mount, DOM not available yet')
// chartRef.value is still null here
})
// MOUNTED: Component is in the DOM
// Good for: DOM manipulation, third-party library initialization
onMounted(async () => {
console.log('Component mounted, DOM available')
// Now safe to access refs and manipulate DOM
initializeChart(chartRef.value)
// Set up observers, event listeners
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(chartRef.value)
// Fetch data that depends on mounted state
const response = await fetch('/api/data')
data.value = await response.json()
})
// BEFORE UNMOUNT: Component is about to be removed
// Good for: Start cleanup, still have DOM access
onBeforeUnmount(() => {
if (chartRef.value) {
destroyChart(chartRef.value)
}
})
// UNMOUNTED: Component removed from DOM
// Good for: Final cleanup of non-DOM resources
onUnmounted(() => {
resizeObserver?.disconnect()
abortController?.abort()
clearInterval(pollInterval)
})
return { data, chartRef }
}
}What are the onActivated and onDeactivated hooks for?
These hooks are specifically for components wrapped in <KeepAlive>. When a kept-alive component is switched away from, it's cached rather than destroyed, triggering onDeactivated. When it's shown again, onActivated runs instead of the normal mount hooks.
Use these for pausing/resuming expensive operations, refreshing stale data when returning to a cached view, or managing timers that shouldn't run when the component is hidden.
Why should you be careful with onUpdated?
The onUpdated hook runs after the DOM has been patched following a data change. The danger is triggering infinite loops—if you modify reactive state inside onUpdated, it triggers another update, which triggers onUpdated again.
onUpdated(() => {
// DON'T do this - causes infinite loop:
// data.value = newValue // Triggers update, calls onUpdated again
// DO this - post-update DOM operations
const container = document.querySelector('.scroll-container')
container.scrollTop = container.scrollHeight
})State Management Questions
State management questions reveal understanding of when to use different approaches.
How do you decide between local state, props, provide/inject, and Pinia?
State management in Vue 3 exists on a spectrum, and choosing the right approach depends on what you're managing.
Local component state is your default for state that only matters to one component:
const count = ref(0)
const user = ref({ name: '', email: '' })Props and events handle parent-child communication:
// Parent
const selectedItem = ref(null)
function handleSelect(item) {
selectedItem.value = item
}
// <ChildList :items="items" @select="handleSelect" />Provide/Inject shares state across deeply nested components without prop drilling:
// Ancestor component
import { provide, ref, readonly } from 'vue'
const user = ref({ name: 'John', role: 'admin' })
provide('user', readonly(user))
provide('setUser', (newUser) => user.value = newUser)
// Deep descendant component
import { inject } from 'vue'
const user = inject('user')
const setUser = inject('setUser')Pinia is the answer when multiple unrelated components need to access and modify the same state.
What is Pinia and how does it compare to Vuex?
Pinia is the official state management library for Vue 3, replacing Vuex. It offers a simpler API, eliminates the mutation concept (just use actions for everything), provides full TypeScript support out of the box, and supports the Composition API style for store definitions.
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: null,
preferences: {}
}),
getters: {
isAuthenticated: (state) => !!state.token,
displayName: (state) => state.user?.name || 'Guest'
},
actions: {
async login(credentials) {
const response = await api.login(credentials)
this.user = response.user
this.token = response.token
},
logout() {
this.user = null
this.token = null
this.$reset() // Built-in method to reset to initial state
}
}
})How do you define Pinia stores using the Composition API syntax?
Pinia supports an alternative syntax that mirrors the Composition API, which some developers find more natural:
// Composition API style stores
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
// Using stores in components
import { useUserStore, useCartStore } from '@/stores'
export default {
setup() {
const userStore = useUserStore()
const cartStore = useCartStore()
// Direct access to state, getters, actions
console.log(userStore.isAuthenticated)
userStore.login({ email, password })
return { userStore, cartStore }
}
}Slots and Component Design Questions
Slots are fundamental to building flexible, reusable components.
What are slots and how do they enable component reusability?
Slots are Vue's content distribution mechanism—they let parent components inject content into predefined locations in child components. What makes them powerful is inversion of control: the child component defines the structure, but the parent decides the content.
<!-- BaseCard.vue -->
<template>
<div class="card">
<header class="card-header">
<slot name="header">Default Header</slot>
</header>
<main class="card-body">
<slot>Default body content</slot>
</main>
<footer class="card-footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- Parent component -->
<template>
<BaseCard>
<template #header>
<h2>Custom Title</h2>
</template>
<p>This goes in the default slot (body)</p>
<template #footer>
<button>Save</button>
<button>Cancel</button>
</template>
</BaseCard>
</template>What are scoped slots and when would you use them?
Sometimes the parent needs access to data that only the child component has. Scoped slots let the child pass data back to the parent's slot content:
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- Pass user data to whoever renders this slot -->
<slot :user="user" :isActive="user.status === 'active'">
{{ user.name }}
</slot>
</li>
</ul>
</template>
<!-- Parent with custom rendering -->
<template>
<UserList :users="users" v-slot="{ user, isActive }">
<div :class="{ 'active-user': isActive }">
<img :src="user.avatar" />
<span>{{ user.name }}</span>
<span class="email">{{ user.email }}</span>
<button @click="editUser(user.id)">Edit</button>
</div>
</UserList>
</template>The parent component now controls exactly how each user is displayed, but the child component controls the iteration and provides the data.
How do you build a flexible data table component with scoped slots?
A data table demonstrates scoped slots' power—the table handles structure while parents customize cell rendering:
<!-- DataTable.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="row.id || index">
<td v-for="col in columns" :key="col.key">
<slot
:name="`cell-${col.key}`"
:value="row[col.key]"
:row="row"
:index="index"
>
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<!-- Parent customizes specific columns -->
<template>
<DataTable :data="products" :columns="columns">
<template #cell-price="{ value }">
<span class="price">${{ value.toFixed(2) }}</span>
</template>
<template #cell-status="{ value, row }">
<StatusBadge :status="value" />
</template>
<template #cell-actions="{ row }">
<button @click="edit(row)">Edit</button>
<button @click="remove(row.id)">Delete</button>
</template>
</DataTable>
</template>Forms and Validation Questions
Form handling reveals practical application development skills.
How does v-model work and how do you implement it for custom components?
The v-model directive is syntactic sugar for binding a value and listening for update events. Understanding this is essential for creating custom form components:
// v-model="form.email" is equivalent to:
// :value="form.email" @input="form.email = $event.target.value"
const form = reactive({
email: '',
password: '',
rememberMe: false
})For custom components, implement the modelValue prop and emit update:modelValue:
<!-- CustomInput.vue -->
<script setup>
const props = defineProps(['modelValue', 'label', 'error'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<div class="custom-input">
<label>{{ label }}</label>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
:class="{ 'has-error': error }"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>
<!-- Usage -->
<CustomInput
v-model="form.email"
label="Email Address"
:error="errors.email"
/>How do you implement form validation in Vue?
Production forms need validation, error handling, and user feedback. Here's a pattern that handles validation on blur and on submit:
<script setup>
import { reactive, computed } from 'vue'
const form = reactive({
email: '',
password: '',
confirmPassword: ''
})
const errors = reactive({
email: null,
password: null,
confirmPassword: null
})
const touched = reactive({
email: false,
password: false,
confirmPassword: false
})
const rules = {
email: (value) => {
if (!value) return 'Email is required'
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email format'
return null
},
password: (value) => {
if (!value) return 'Password is required'
if (value.length < 8) return 'Password must be at least 8 characters'
if (!/[A-Z]/.test(value)) return 'Password must contain uppercase letter'
return null
},
confirmPassword: (value) => {
if (!value) return 'Please confirm your password'
if (value !== form.password) return 'Passwords do not match'
return null
}
}
function validate(field) {
errors[field] = rules[field](form[field])
}
function handleBlur(field) {
touched[field] = true
validate(field)
}
const isValid = computed(() =>
Object.keys(rules).every(field => rules[field](form[field]) === null)
)
async function handleSubmit() {
Object.keys(rules).forEach(field => {
touched[field] = true
validate(field)
})
if (!isValid.value) return
await submitForm(form)
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div class="field">
<label for="email">Email</label>
<input
id="email"
v-model="form.email"
type="email"
@blur="handleBlur('email')"
:class="{ 'is-error': touched.email && errors.email }"
/>
<span v-if="touched.email && errors.email" class="error">
{{ errors.email }}
</span>
</div>
<button type="submit" :disabled="!isValid">Register</button>
</form>
</template>Vue Router Questions
Navigation guards are essential for authentication and route protection.
What are Vue Router navigation guards and what types exist?
Navigation guards are hooks that let you intercept navigations and decide whether to allow, redirect, or cancel them. There are three levels of guards:
Global guards apply to all route changes:
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
router.beforeEach(async (to, from) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return {
name: 'Login',
query: { redirect: to.fullPath }
}
}
if (to.meta.roles && !to.meta.roles.includes(auth.user?.role)) {
return { name: 'Forbidden' }
}
})
router.afterEach((to, from) => {
document.title = to.meta.title || 'My App'
analytics.page(to.fullPath)
})Per-route guards are defined in the route configuration:
const routes = [
{
path: '/admin',
component: AdminLayout,
beforeEnter: (to, from) => {
const auth = useAuthStore()
if (auth.user?.role !== 'admin') {
return { name: 'Forbidden' }
}
}
}
]Component guards are defined in the component itself:
<script>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
export default {
setup() {
const hasUnsavedChanges = ref(false)
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const confirmed = window.confirm('You have unsaved changes. Leave anyway?')
if (!confirmed) return false
}
})
onBeforeRouteUpdate(async (to, from) => {
// When params change but component is reused: /users/1 -> /users/2
await fetchUser(to.params.id)
})
return { hasUnsavedChanges }
}
}
</script>How do you implement authentication with route guards?
A practical authentication flow combines global guards with route meta:
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (to.meta.public) return
if (!auth.initialized) {
await auth.initialize()
}
if (!auth.isAuthenticated) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
if (to.meta.requiresVerification && !auth.user.emailVerified) {
return { name: 'VerifyEmail' }
}
})
const routes = [
{ path: '/login', component: Login, meta: { public: true } },
{ path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
{ path: '/settings', component: Settings, meta: { requiresAuth: true, requiresVerification: true } }
]Performance and Testing Questions
Performance optimization and testing questions reveal senior-level understanding.
How do async components and lazy loading work in Vue?
Async components let you split your bundle so users only download code when needed. Vue has first-class support for this with defineAsyncComponent:
import { defineAsyncComponent } from 'vue'
// Basic async component
const AsyncDashboard = defineAsyncComponent(() =>
import('./components/Dashboard.vue')
)
// With loading and error states
const AsyncHeavyChart = defineAsyncComponent({
loader: () => import('./components/HeavyChart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 10000
})Route-based splitting is particularly effective:
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
},
{
path: '/analytics',
component: () => import(
/* webpackChunkName: "analytics" */
'./views/Analytics.vue'
)
}
]How do you use Suspense with async components?
Suspense is Vue 3's way to handle async component trees, showing fallback content while the async content loads:
<template>
<Suspense>
<template #default>
<AsyncDashboard />
</template>
<template #fallback>
<LoadingState />
</template>
</Suspense>
</template>
<script setup>
const AsyncDashboard = defineAsyncComponent(
() => import('./Dashboard.vue')
)
</script>How do you test Vue components?
Testing Vue components effectively requires understanding what to test and how. Vue Test Utils is the standard library, and the key principle is testing behavior, not implementation:
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserGreeting from './UserGreeting.vue'
describe('UserGreeting', () => {
it('displays user name in greeting', () => {
const wrapper = mount(UserGreeting, {
props: {
user: { name: 'John', email: 'john@example.com' }
}
})
expect(wrapper.find('h1').text()).toBe('Hello, John')
})
it('displays Guest when no user provided', () => {
const wrapper = mount(UserGreeting, {
props: { user: null }
})
expect(wrapper.find('h1').text()).toBe('Hello, Guest')
})
it('toggles details visibility on button click', async () => {
const wrapper = mount(UserGreeting, {
props: {
user: { name: 'John', email: 'john@example.com' }
}
})
expect(wrapper.find('.details').exists()).toBe(false)
await wrapper.find('button').trigger('click')
expect(wrapper.find('.details').exists()).toBe(true)
expect(wrapper.find('.details').text()).toContain('john@example.com')
await wrapper.find('button').trigger('click')
expect(wrapper.find('.details').exists()).toBe(false)
})
})How do you test composables?
Composables can be tested directly without mounting components:
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('starts with initial value', () => {
const { count } = useCounter(5)
expect(count.value).toBe(5)
})
it('increments count', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
it('computes doubled value', () => {
const { count, doubled, increment } = useCounter(3)
expect(doubled.value).toBe(6)
increment()
expect(doubled.value).toBe(8)
})
})Quick Reference
| Concept | Options API | Composition API |
|---|---|---|
| Reactive state | data() | ref(), reactive() |
| Computed | computed: {} | computed() |
| Methods | methods: {} | Plain functions |
| Watch | watch: {} | watch(), watchEffect() |
| Lifecycle | mounted() | onMounted() |
| Props | props: [] | defineProps() |
| Events | $emit() | defineEmits() |
| Template refs | this.$refs | ref() + ref="name" |
| State Management | Use When |
|---|---|
| Local state | Single component needs it |
| Props/Events | Parent-child communication |
| Provide/Inject | Deep component tree, avoiding prop drilling |
| Pinia | Multiple unrelated components share state |
| Composables | Encapsulating and reusing stateful logic |
Related Articles
If you found this helpful, check out these related guides:
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- JavaScript Closures Interview Guide - Essential for understanding Vue's Composition API and reactivity
- 9 Advanced React Interview Questions - Compare React patterns with Vue's approach
- TypeScript Type vs Interface - TypeScript knowledge for Vue 3 with full type support
- JavaScript Event Loop Interview Guide - Understand async behavior in Vue applications
