# Essential Vue.js Interview Questions: 25 Questions to Land the Job > Prepare for Vue.js interviews with these 25 essential questions. From reactivity to composables, master the key concepts to ace your next interview. - Published: 2026-02-05 - Updated: 2026-03-31 - Author: SharpSkill - Tags: vue.js, interview, frontend, javascript, technical questions - Reading time: 15 min --- Vue.js interviews assess far more than framework syntax. Recruiters want to understand mastery of the reactivity system, code organization with the Composition API, and the ability to solve real-world performance and architecture challenges. > **Preparation Advice** > > Each question includes a detailed answer and code examples. For technical interviews, practicing by explaining concepts out loud, as during a real interview, is recommended. ## Fundamental Vue.js Questions ### 1. What is the difference between ref and reactive in Vue 3? This question tests understanding of the reactivity system, which is fundamental to Vue 3. The main difference lies in the types of data handled and the access syntax. `ref` creates a reactive reference for primitive values (string, number, boolean) and requires `.value` to access the value in the script. `reactive` creates a reactive proxy for objects and allows direct property access. ```javascript // Comparative example ref vs reactive import { ref, reactive } from 'vue' // ref: for primitives // Requires .value in the script const count = ref(0) count.value++ // Access with .value // reactive: for complex objects // Direct property access const user = reactive({ name: 'Alice', age: 25 }) user.age++ // No .value needed // Warning: reactive loses reactivity if reassigned // user = { name: 'Bob', age: 30 } // ❌ Breaks reactivity Object.assign(user, { name: 'Bob', age: 30 }) // ✅ Correct ``` The general rule: use `ref` for simple values and `reactive` for structured objects with multiple related properties. ### 2. How does Vue 3's reactivity system work? Vue 3 uses JavaScript Proxies (ES6) to intercept operations on reactive objects. Unlike Vue 2 which used `Object.defineProperty`, this approach detects property additions and deletions dynamically. ```javascript // Simplified demonstration of the reactivity principle // Vue uses Proxies to track dependencies const handler = { // Intercept reading get(target, key, receiver) { track(target, key) // Register the dependency return Reflect.get(target, key, receiver) }, // Intercept writing set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) trigger(target, key) // Trigger updates return result } } // Creating a reactive proxy const reactiveObject = new Proxy(originalObject, handler) ``` Proxy advantages include: detection of new properties, Map/Set support, and better performance on large objects. ### 3. Explain the difference between computed and watch `computed` and `watch` serve different needs in reactivity management. **Computed**: calculates a derived value from other reactive data. Values are cached and only recalculate when dependencies change. Ideal for data transformations. **Watch**: executes side effects in response to changes. Useful for API calls, DOM interactions, or asynchronous operations. ```javascript // Computed vs watch comparison import { ref, computed, watch } from 'vue' const firstName = ref('John') const lastName = ref('Doe') // computed: derived value with cache // Recalculates only if firstName or lastName changes const fullName = computed(() => { console.log('Computing full name') // Called only once return `${firstName.value} ${lastName.value}` }) // Multiple accesses = single execution (cached) console.log(fullName.value) // "John Doe" console.log(fullName.value) // No recalculation // watch: side effect without cache // Executed on every change watch(firstName, async (newName, oldName) => { // Side effect: API call await saveToServer({ firstName: newName }) console.log(`Name changed from ${oldName} to ${newName}`) }) ``` ### 4. What is the Virtual DOM and how does Vue use it? The Virtual DOM is a lightweight JavaScript representation of the real DOM. Vue maintains a virtual tree in memory and calculates differences (diffing) between the previous and new state to apply only necessary changes to the real DOM. ```javascript // Conceptual representation of the Virtual DOM // Vue creates this structure internally const vnode = { type: 'div', props: { class: 'container', id: 'app' }, children: [ { type: 'h1', props: {}, children: 'Title' }, { type: 'p', props: {}, children: 'Paragraph content' } ] } // When changes occur, Vue compares vnodes // and updates only the modified elements ``` Vue 3 optimizations include: static node hoisting, patch flags to identify change types, and compiler tree-shaking. ### 5. How do you handle communication between unrelated components? Several patterns exist for communication between components without a direct parent-child relationship. ```javascript // Solution 1: Event Bus (small applications) // eventBus.js import { ref } from 'vue' const bus = ref(new Map()) export function useEventBus() { // Emit an event const emit = (event, payload) => { const callbacks = bus.value.get(event) || [] callbacks.forEach(cb => cb(payload)) } // Listen to an event const on = (event, callback) => { if (!bus.value.has(event)) { bus.value.set(event, []) } bus.value.get(event).push(callback) } return { emit, on } } ``` ```javascript // Solution 2: Provide/Inject for nested components // Ancestor import { provide, ref } from 'vue' const sharedState = ref('shared value') provide('stateKey', sharedState) // Descendant (any level) import { inject } from 'vue' const state = inject('stateKey') ``` For more complex applications, Pinia remains the recommended solution for global state management. ## Composition API Questions ### 6. What are the advantages of Composition API over Options API? The Composition API offers several structural advantages over the traditional Options API. **Organization by feature**: code related to the same feature is grouped together, unlike Options API which separates by type (data, methods, computed). **Reuse via composables**: easy extraction of logic into reusable functions. **Better TypeScript support**: natural type inference without decorators. ```javascript // Options API: code fragmented by type export default { data() { return { searchQuery: '', results: [] } }, computed: { hasResults() { return this.results.length > 0 } }, methods: { async search() { this.results = await fetchResults(this.searchQuery) } }, watch: { searchQuery: 'search' } } // Composition API: code grouped by feature import { ref, computed, watch } from 'vue' export function useSearch() { const searchQuery = ref('') const results = ref([]) const hasResults = computed(() => results.value.length > 0) const search = async () => { results.value = await fetchResults(searchQuery.value) } watch(searchQuery, search) return { searchQuery, results, hasResults, search } } ``` ### 7. How do you create a reusable composable? Composables are functions that encapsulate reactive logic. Conventions include: `use` prefix, returning an object with state and methods, and handling cleanup. ```javascript // composables/useLocalStorage.js import { ref, watch } from 'vue' // Composable to synchronize state with localStorage export function useLocalStorage(key, defaultValue) { // Retrieve initial value from localStorage const storedValue = localStorage.getItem(key) const data = ref( storedValue ? JSON.parse(storedValue) : defaultValue ) // Synchronize changes to localStorage watch( data, (newValue) => { if (newValue === null) { localStorage.removeItem(key) } else { localStorage.setItem(key, JSON.stringify(newValue)) } }, { deep: true } // Observe nested objects ) return data } // Usage in a component const theme = useLocalStorage('theme', 'light') const userPrefs = useLocalStorage('prefs', { notifications: true }) ``` > **Naming Convention** > > Composables follow the `useXxx` convention to indicate their reusable nature. This convention improves readability and makes it easier to identify reactive dependencies. ### 8. Explain watchEffect vs watch `watchEffect` and `watch` both react to changes, but with different approaches. **watchEffect**: runs immediately and re-runs automatically when its reactive dependencies change. Dependencies are tracked automatically. **watch**: observes specific sources and provides old and new values. More control over when it triggers. ```javascript // watchEffect vs watch comparison import { ref, watch, watchEffect } from 'vue' const userId = ref(1) const userData = ref(null) // watchEffect: automatic tracking // Runs immediately watchEffect(async () => { // userId is automatically tracked const response = await fetch(`/api/users/${userId.value}`) userData.value = await response.json() }) // watch: explicit sources with old values watch(userId, async (newId, oldId) => { console.log(`User changed from ${oldId} to ${newId}`) const response = await fetch(`/api/users/${newId}`) userData.value = await response.json() }, { immediate: true // Run immediately like watchEffect }) // watchEffect with cleanup watchEffect((onCleanup) => { const controller = new AbortController() fetch(`/api/users/${userId.value}`, { signal: controller.signal }).then(/* ... */) // Cleanup: cancel previous request onCleanup(() => controller.abort()) }) ``` ### 9. How do you handle props with TypeScript in script setup? The ` ``` ## Performance Questions ### 10. What performance optimization techniques do you know? Vue 3 offers several performance optimization mechanisms. ```javascript // 1. v-once: single render for static content // 2. v-memo: conditional memoization // 3. shallowRef/shallowReactive: shallow reactivity import { shallowRef, triggerRef } from 'vue' // Only tracks ref replacement, not internal mutations const largeList = shallowRef([/* thousands of elements */]) // Force update after mutation largeList.value.push(newItem) triggerRef(largeList) // Manually trigger re-render ``` ```javascript // 4. Async components for code-splitting import { defineAsyncComponent } from 'vue' const HeavyComponent = defineAsyncComponent({ loader: () => import('./HeavyComponent.vue'), loadingComponent: LoadingSpinner, delay: 200, // Delay before showing loading errorComponent: ErrorDisplay, timeout: 3000 }) // 5. KeepAlive for component caching ``` ### 11. How do you avoid unnecessary re-renders? Unnecessary re-renders can impact performance. Several strategies help minimize them. ```javascript // Problem: function created on each render // Solution: use a method or ref ``` ```javascript // Using computed for expensive calculations import { computed } from 'vue' // ❌ Recalculated on every render const getFilteredItems = () => { return items.value.filter(/* complex logic */) } // ✅ Cached, recalculated only if items changes const filteredItems = computed(() => { return items.value.filter(/* complex logic */) }) ``` ### 12. Explain lazy loading for components and routes Lazy loading allows loading code on demand, reducing the initial bundle size. ```javascript // router/index.js import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', // Immediate loading (main bundle) component: () => import('@/views/Home.vue') }, { path: '/dashboard', // Separate chunk with custom name component: () => import( /* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue' ), // Lazy loading child routes children: [ { path: 'analytics', component: () => import('@/views/Analytics.vue') } ] }, { path: '/admin', // Prefetch on link hover component: () => import('@/views/Admin.vue'), meta: { prefetch: true } } ] }) export default router ``` ## Vue Router Questions ### 13. How do you protect routes with guards? Navigation guards allow controlling access to routes. ```javascript // router/index.js import { createRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' const router = createRouter({ routes: [ { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true, roles: ['admin', 'user'] } } ] }) // Global guard: checks authentication router.beforeEach(async (to, from, next) => { const auth = useAuthStore() // Public route if (!to.meta.requiresAuth) { return next() } // Check authentication if (!auth.isAuthenticated) { return next({ path: '/login', query: { redirect: to.fullPath } }) } // Check roles if specified if (to.meta.roles && !to.meta.roles.includes(auth.user.role)) { return next('/unauthorized') } next() }) // Component-level guard export default { beforeRouteEnter(to, from, next) { // No access to this here next(vm => { // Access component instance via vm vm.loadData() }) }, beforeRouteLeave(to, from, next) { // Confirm before leaving if form modified if (this.hasUnsavedChanges) { const answer = confirm('Leave without saving?') next(answer) } else { next() } } } ``` ### 14. How do you pass props to routes? Vue Router allows decoupling components from route parameters. ```javascript // Route configuration with props const routes = [ { path: '/user/:id', component: UserProfile, // Boolean mode: passes params as props props: true }, { path: '/search', component: SearchResults, // Function mode: custom transformation props: (route) => ({ query: route.query.q, page: parseInt(route.query.page) || 1, filters: route.query.filters?.split(',') || [] }) }, { path: '/static', component: StaticPage, // Object mode: static props props: { sidebar: true, theme: 'dark' } } ] ``` ```javascript // UserProfile.vue // SearchResults.vue ``` ## Pinia and State Management Questions ### 15. What are the differences between Pinia and Vuex? Pinia is the official state manager for Vue 3, replacing Vuex with a simplified API. | Feature | Vuex | Pinia | |---------|------|-------| | Mutations | Required | Not needed | | Modules | Complex configuration | Independent stores | | TypeScript | Limited support | Native and complete | | API | Options | Composition + Options | | DevTools | Support | Full support | ```javascript // Pinia Store with Composition API import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCartStore = defineStore('cart', () => { // State const items = ref([]) const discountCode = ref(null) // Getters (computed) const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) ) const totalPrice = computed(() => { const subtotal = items.value.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) return discountCode.value ? subtotal * 0.9 : subtotal }) // Actions (direct functions) function addItem(product) { const existing = items.value.find(i => i.id === product.id) if (existing) { existing.quantity++ } else { items.value.push({ ...product, quantity: 1 }) } } function removeItem(productId) { const index = items.value.findIndex(i => i.id === productId) if (index > -1) { items.value.splice(index, 1) } } async function checkout() { const response = await api.createOrder(items.value) items.value = [] return response } return { items, discountCode, totalItems, totalPrice, addItem, removeItem, checkout } }) ``` ### 16. How do you persist Pinia store state? Persistence allows maintaining state between user sessions. ```javascript // plugins/piniaPersistedState.js import { watch } from 'vue' export function createPersistedState(options = {}) { const { key = 'pinia', storage = localStorage, paths = null } = options return ({ store }) => { // Restore state on startup const savedState = storage.getItem(`${key}-${store.$id}`) if (savedState) { store.$patch(JSON.parse(savedState)) } // Persist changes watch( () => store.$state, (state) => { const toSave = paths ? paths.reduce((acc, path) => { acc[path] = state[path] return acc }, {}) : state storage.setItem( `${key}-${store.$id}`, JSON.stringify(toSave) ) }, { deep: true } ) } } // main.js import { createPinia } from 'pinia' import { createPersistedState } from './plugins/piniaPersistedState' const pinia = createPinia() pinia.use(createPersistedState({ key: 'app-state', paths: ['user', 'preferences'] // Persist only these keys })) ``` > **Sensitive Data** > > Avoid persisting sensitive data (tokens, passwords) in localStorage. Use secure solutions like httpOnly cookies for authentication tokens. ## Advanced Questions ### 17. How do you implement a Vue plugin system? Plugins allow extending Vue with global features. ```javascript // plugins/analyticsPlugin.js export const AnalyticsPlugin = { install(app, options = {}) { const { trackingId, debug = false } = options // Global injection available in all components const analytics = { trackEvent(category, action, label) { if (debug) { console.log('Analytics:', { category, action, label }) } // Logic to send to analytics service window.gtag?.('event', action, { event_category: category, event_label: label }) }, trackPage(path) { window.gtag?.('config', trackingId, { page_path: path }) } } // Make available via inject app.provide('analytics', analytics) // Add global property (discouraged in Composition API) app.config.globalProperties.$analytics = analytics // Custom directive for click tracking app.directive('track', { mounted(el, binding) { el.addEventListener('click', () => { analytics.trackEvent('click', binding.value, el.textContent) }) } }) // Automatic route change tracking app.mixin({ mounted() { if (this.$route) { analytics.trackPage(this.$route.path) } } }) } } // main.js import { AnalyticsPlugin } from './plugins/analyticsPlugin' app.use(AnalyticsPlugin, { trackingId: 'UA-XXXXX-X', debug: import.meta.env.DEV }) ``` ### 18. Explain Render Functions and their usefulness Render functions offer total control over rendering, useful for highly dynamic components. ```javascript // components/DynamicHeading.js import { h } from 'vue' // Functional component with render function export const DynamicHeading = { props: { level: { type: Number, default: 1, validator: (v) => v >= 1 && v <= 6 } }, setup(props, { slots }) { // h() creates a vnode // Arguments: type, props, children return () => h( `h${props.level}`, { class: 'dynamic-heading' }, slots.default?.() ) } } // Component with complex conditional logic export const ConditionalWrapper = { props: ['condition', 'wrapper'], setup(props, { slots }) { return () => { if (props.condition) { return h(props.wrapper, null, slots.default?.()) } return slots.default?.() } } } // Usage Level 2 Title Conditional content ``` ### 19. How do you test Vue components with Vitest? Unit tests validate the isolated behavior of components. ```javascript // components/__tests__/Counter.spec.js import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import Counter from '../Counter.vue' describe('Counter', () => { it('displays the initial value', () => { const wrapper = mount(Counter, { props: { initialValue: 5 } }) expect(wrapper.text()).toContain('5') }) it('increments the value on click', async () => { const wrapper = mount(Counter) await wrapper.find('button.increment').trigger('click') expect(wrapper.text()).toContain('1') }) it('emits an event when changed', async () => { const wrapper = mount(Counter) await wrapper.find('button.increment').trigger('click') expect(wrapper.emitted('change')).toBeTruthy() expect(wrapper.emitted('change')[0]).toEqual([1]) }) it('calls the service on submit', async () => { const mockSubmit = vi.fn() const wrapper = mount(Counter, { global: { provide: { submitService: mockSubmit } } }) await wrapper.find('form').trigger('submit') expect(mockSubmit).toHaveBeenCalled() }) }) ``` ### 20. How do you handle errors globally in Vue? Vue 3 offers several mechanisms to capture and handle errors. ```javascript // main.js import { createApp } from 'vue' const app = createApp(App) // Global handler for component errors app.config.errorHandler = (err, instance, info) => { // err: the error // instance: the component instance // info: string describing where the error occurred console.error('Vue error:', err) console.error('Component:', instance?.$options?.name) console.error('Info:', info) // Send to monitoring service errorTracker.captureException(err, { component: instance?.$options?.name, info }) } // Handler for warnings (dev only) app.config.warnHandler = (msg, instance, trace) => { console.warn('Vue warning:', msg) } ``` ```javascript // ErrorBoundary component ``` ## Best Practices Questions ### 21. What naming conventions should be followed in Vue? Naming conventions improve code readability and maintainability. ```javascript // Component naming // PascalCase for files and names // BaseButton.vue, AppHeader.vue, TheNavbar.vue // Props: camelCase in JS, kebab-case in template defineProps<{ userName: string // JS isActive: boolean // JS }>() // // Events: camelCase with action prefix const emit = defineEmits<{ (e: 'updateValue', value: string): void // ✅ (e: 'submit'): void // ✅ (e: 'value-updated'): void // ❌ Avoid }>() // Composables: use prefix // useAuth.js, useFetch.js, useLocalStorage.js // Pinia stores: use prefix + Store suffix // useUserStore, useCartStore, useSettingsStore // Constants: SCREAMING_SNAKE_CASE const MAX_RETRY_COUNT = 3 const API_BASE_URL = '/api/v1' ``` ### 22. How do you structure a large-scale Vue project? A modular structure facilitates navigation and maintenance. ``` src/ ├── assets/ # Static files ├── components/ │ ├── ui/ # Generic components (Button, Modal) │ └── common/ # Reusable business components ├── composables/ # Reusable logic │ ├── useAuth.js │ └── useFetch.js ├── layouts/ # Page layouts │ ├── DefaultLayout.vue │ └── AuthLayout.vue ├── modules/ # Functional modules │ ├── auth/ │ │ ├── components/ │ │ ├── composables/ │ │ ├── stores/ │ │ └── views/ │ └── dashboard/ ├── plugins/ # Vue plugins ├── router/ │ ├── index.js │ └── guards.js ├── stores/ # Global Pinia stores ├── types/ # TypeScript types ├── utils/ # Pure utilities └── views/ # Pages/Routes ``` ### 23. When to use v-if vs v-show? The choice between `v-if` and `v-show` depends on toggle frequency. ```javascript // v-if: low initial cost, expensive toggle // Removes/adds element from DOM // Ideal for: rarely modified conditions // v-show: higher initial cost, fast toggle // Uses display: none // Ideal for: frequent toggles ``` ### 24. How do you optimize lists with v-for? List optimization is crucial for performance with many elements. ```javascript // Always use :key with a unique stable identifier // Filtering and sorting: use computed // Virtualization for very long lists ``` ### 25. Explain the Renderless component pattern Renderless components encapsulate logic without imposing HTML structure. ```javascript // components/MouseTracker.vue ``` ```javascript // Usage: full control over rendering ``` This pattern completely separates logic from presentation, maximizing reusability. ## Conclusion These 25 questions cover the essential concepts evaluated in Vue.js interviews: - ✅ **Reactivity**: `ref`, `reactive`, `computed`, `watch` - ✅ **Composition API**: composables, `script setup`, TypeScript - ✅ **Performance**: lazy loading, virtualization, optimizations - ✅ **Vue Router**: guards, props, navigation - ✅ **Pinia**: stores, persistence, async actions - ✅ **Best practices**: structure, conventions, advanced patterns Effective preparation combines theoretical understanding with code practice. Each concept covered here can be the subject of deeper follow-up questions during the interview. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/vue-nuxt/essential-vuejs-interview-questions