Vue Interview Questions
Vue 3 & frontend interview guide — 121 unique questions with theory, examples, and interview-ready answers across Composition API, Pinia, routing, Nuxt, and performance.
Introduction
This guide merges 121 unique interview questions from 20 preparation notes into one reference — Vue fundamentals, Composition API, reactivity, Pinia, Vue Router, Nuxt, performance, JavaScript, TypeScript, and architecture.
Questions are grouped into 15 topic areas, deduplicated by title, and stripped of company-specific references. Each entry keeps theory, trade-offs where relevant, concise interview answers, and practical code examples.
Quick index
Vue Fundamentals
| # | Section |
|---|---|
| 1 | Custom Directives |
| 2 | Options API vs Composition API |
| 3 | Single File Components |
| 4 | Template Syntax & Directives |
| 5 | Vue 2 vs Vue 3 |
| 6 | Vue vs React |
| 7 | What is Vue.js? |
Composition API & Reactivity
Components & Templates
State Management
Routing & Navigation
| # | Section |
|---|---|
| 54 | Lazy-Loaded Routes |
| 55 | Navigation Guards |
| 56 | Route Configuration |
| 57 | useRoute & useRouter |
| 58 | Vue Router |
| 59 | Vue Router Basics |
API & Data Fetching
| # | Section |
|---|---|
| 60 | @tanstack/vue-query |
| 61 | Composition API |
| 62 | CORS |
| 63 | Long Polling vs WebSockets |
| 64 | useAsyncData / useFetch (Nuxt) |
| 65 | Vue Query |
| 66 | WebSockets |
Performance
| # | Section |
|---|---|
| 67 | defineAsyncComponent |
| 68 | Keep-alive |
| 69 | Performance — v-memo |
| 70 | Performance Optimization |
| 71 | Suspense |
| 72 | Teleport |
| 73 | v-memo |
| 74 | Virtual Scrolling |
Nuxt & SSR
| # | Section |
|---|---|
| 75 | CSR vs SSR |
| 76 | Hydration Mismatch |
| 77 | Nuxt Fullstack |
| 78 | Nuxt.js & SSR |
Architecture & Patterns
| # | Section |
|---|---|
| 79 | Feature-Based Structure |
| 80 | Plugins |
| 81 | render Functions & h() |
| 82 | Vue Architecture |
| 83 | Vue Design Patterns |
JavaScript Core
| # | Section |
|---|---|
| 84 | Async |
| 85 | Closure |
| 86 | Debouncing |
| 87 | Debouncing & Throttling |
| 88 | Event Loop |
| 89 | Garbage Collection |
TypeScript
| # | Section |
|---|---|
| 90 | TypeScript |
| 91 | TypeScript with Vue |
Security & Accessibility
| # | Section |
|---|---|
| 92 | Accessibility |
Testing & Tooling
| # | Section |
|---|---|
| 93 | Vitest + Vue Test Utils |
Behavioral & Soft Skills
| # | Section |
|---|---|
| 94 | Behavioral Questions |
General & Advanced
Vue Fundamentals
1. Custom Directives
app.directive('focus', {
mounted(el) {
el.focus()
},
})
// <input v-focus />2. Options API vs Composition API
Theory
Options API — organize code by option type (data, methods, computed, watch).
Composition API — organize code by feature/concern using setup() or <script setup>.
Use Options for simple components or legacy codebases. Use Composition for complex logic, reuse via composables, and TypeScript.
Real Example
<!-- Options API -->
<script>
export default {
data() {
return { count: 0 }
},
computed: {
double() {
return this.count * 2
},
},
methods: {
increment() {
this.count++
},
},
}
</script>
<!-- Composition API -->
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>3. Single File Components
Theory
.vue files combine <script>, <template>, <style scoped> — colocated component logic and styling.
<script setup>
defineProps({ title: String })
</script>
<template>
<h2>{{ title }}</h2>
</template>
<style scoped>
h2 {
color: #42b883;
}
</style>4. Template Syntax & Directives
Theory
| Directive | Purpose |
|---|---|
v-bind / : | Bind attribute |
v-on / @ | Event listener |
v-model | Two-way binding |
v-if / v-else / v-show | Conditional render |
v-for | List rendering |
v-slot / # | Named/scoped slots |
v-once | Render once, skip updates |
v-memo | Memoize sub-tree (Vue 3.2+) |
v-if vs v-show: v-if removes from DOM; v-show toggles display: none.
5. Vue 2 vs Vue 3
Theory
| Vue 2 | Vue 3 | |
|---|---|---|
| Reactivity | Object.defineProperty | Proxy |
| API | Options API primary | Composition API + Options |
| Multiple root nodes | No (single root) | Yes (fragments) |
| TypeScript | Awkward | First-class |
| State | Vuex 3/4 | Pinia (recommended) |
| Bundle size | Larger | Tree-shakeable, smaller |
| Performance | Good | Faster diff, better memory |
6. Vue vs React
Theory
| Vue | React | |
|---|---|---|
| Type | Framework (progressive) | Library (UI only) |
| Template | HTML templates + directives | JSX |
| Reactivity | Automatic (Proxy) | Manual (setState/hooks) |
| State | Pinia built-in ecosystem | Redux/Zustand external |
| Learning curve | Gentler | Steeper (hooks rules) |
| Two-way binding | v-model native | Controlled components |
| Topic | One-liner |
|---|---|
| Vue 3 reactivity | Proxy — ref for primitives, reactive for objects |
| Composition API | Feature-based logic, composables for reuse |
| computed vs watch | Derived value vs side effect on change |
| Pinia | defineStore — state, getters, actions, no mutations |
| v-if vs v-show | Mount/unmount vs display toggle |
| provide/inject | Skip prop drilling across tree |
| Composables | Reusable setup logic — useFetch, useDebounce |
7. What is Vue.js?
Theory
Vue.js is a progressive JavaScript framework for building user interfaces. It combines a reactive data layer with a component-based architecture. "Progressive" means you can adopt it incrementally — drop a script tag on a page or build a full SPA with Vue Router and Pinia.
Pros & Cons
| Pros | Cons |
|---|---|
| Gentle learning curve | Two API styles (Options + Composition) |
| Excellent docs | Smaller job market vs React in some regions |
| Built-in reactivity | Ecosystem split between Vuex/Pinia migration |
| Single-file components (.vue) | Template syntax unfamiliar to JSX devs |
Real Example
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>Composition API & Reactivity
8. Composable vs Mixin
9. Composables (Custom Hooks)
Theory
Composables are reusable functions using Composition API — Vue's equivalent of React custom
hooks. Must start with use.
Real Example
// composables/useFetch.js
import { ref, watchEffect, onScopeDispose } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
let controller
watchEffect(async (onCleanup) => {
controller = new AbortController()
loading.value = true
error.value = null
onCleanup(() => controller.abort())
try {
const res = await fetch(url, { signal: controller.signal })
if (!res.ok) throw new Error(res.statusText)
data.value = await res.json()
} catch (e) {
if (e.name !== 'AbortError') error.value = e.message
} finally {
loading.value = false
}
})
return { data, loading, error }
}10. Composables in Conditions?
// ❌ if (loggedIn) { const { user } = useAuth(); }
// ✅ const { user } = useAuth(); then use v-if in template11. Computed vs Watch vs watchEffect
Theory
| computed | watch | watchEffect | |
|---|---|---|---|
| Purpose | Derived value (cached) | React to specific source | Auto-track dependencies |
| Returns | Ref | Stop handle | Stop handle |
| Lazy | Yes — only when accessed | No — on change | Immediate |
| Use for | Filtered lists, totals | API calls on ID change | Side effects with auto deps |
Real Example
<script setup>
import { ref, computed, watch, watchEffect } from 'vue'
const query = ref('')
const products = ref([/* ... */])
const filtered = computed(() => products.value.filter((p) => p.name.includes(query.value)))
watch(query, (newQ, oldQ) => {
analytics.track('search', { query: newQ, prev: oldQ })
})
watchEffect(() => {
document.title = query.value ? `Search: ${query.value}` : 'Shop'
})
</script>12. computed()
const fullName = computed({
get: () => `${first.value} ${last.value}`,
set: (val) => {
;[first.value, last.value] = val.split(' ')
},
})13. defineExpose()
defineExpose({ focus: () => inputRef.value?.focus() })14. nextTick
Theory
Wait for DOM update after reactive state change before accessing updated DOM.
count.value++
await nextTick()
console.log(el.value.textContent) // updated DOM15. reactive()
16. Reactivity Internals
17. Reactivity System
Theory
Vue 3 reactivity uses Proxy to intercept get/set on objects.
| API | Use for |
|---|---|
ref() | Primitives — access via .value in JS |
reactive() | Objects/arrays — no .value |
readonly() | Prevent mutations |
toRef() / toRefs() | Destructure reactive without losing reactivity |
shallowRef() | Only .value change triggers update |
Real Example
import { ref, reactive, toRefs } from 'vue'
const count = ref(0)
count.value++ // ✅
const state = reactive({ name: 'Amit', age: 28 })
state.name = 'Rahul' // ✅ reactive
// ❌ Destructuring breaks reactivity
const { name } = state
// ✅ Fix with toRefs
const { name, age } = toRefs(state)
name.value = 'Priya'18. Reactivity Utilities
import { ref, shallowRef, readonly, toRef, triggerRef } from 'vue'
const state = shallowRef({ huge: 'data' }) // only .value change triggers update
const readOnlyState = readonly(state)
const itemRef = toRef(state.value, 'key')
triggerRef(state) // force update for shallowRef19. ref vs reactive
Theory
ref — any value, access .value in script. reactive — objects only, no .value.
const count = ref(0)
const state = reactive({ items: [] })
count.value++
state.items.push(item)20. ref()
21. script setup
<script setup>
// Top-level bindings auto-exposed to template
// defineProps, defineEmits, defineExpose are compiler macros
const props = defineProps({ id: String })
const emit = defineEmits(['save'])
</script>22. toRef / toRefs
23. useFetch Composable
import { ref, watch, toValue } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
let controller
async function execute() {
const resolvedUrl = toValue(url)
if (!resolvedUrl) return
controller?.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
const res = await fetch(resolvedUrl, {
signal: controller.signal,
...options.fetchOptions,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
if (e.name !== 'AbortError') error.value = e.message
} finally {
loading.value = false
}
}
watch(() => toValue(url), execute, { immediate: true })
return { data, loading, error, refetch: execute }
}<script setup>
import { useFetch } from '@/composables/useFetch'
const userId = ref('1')
const {
data: user,
loading,
error,
refetch,
} = useFetch(computed(() => `https://jsonplaceholder.typicode.com/users/${userId.value}`))
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">
Error: {{ error }}
<button @click="refetch">Retry</button>
</div>
<div v-else-if="user">
<h1>{{ user.name }}</h1>
</div>
</template>24. watch vs watchEffect
watch(userId, (id) => fetchUser(id))
watchEffect(() => {
document.title = user.value?.name ?? 'App'
})25. watch vs watchEffect vs computed
| computed | watch | watchEffect | |
|---|---|---|---|
| Use | Derived value | React to source | Auto-tracked side effect |
| Lazy | Yes | No | Immediate |
26. watch()
watch(source, (newVal, oldVal) => {}, { immediate: true, deep: true, flush: 'post' })
watch([a, b], ([newA, newB]) => {})27. watchEffect()
28. Why Composables?
Components & Templates
29. Components — Props, Emits, Slots
Theory
- Props — parent → child (read-only)
- Emits — child → parent events
- Slots — parent injects content into child
defineProps and defineEmits are compiler macros in <script setup>.
Real Example
<!-- Child: UserCard.vue -->
<script setup>
const props = defineProps({
name: { type: String, required: true },
role: { type: String, default: 'user' },
})
const emit = defineEmits(['edit', 'delete'])
</script>
<template>
<div class="card">
<slot name="avatar" />
<h3>{{ name }}</h3>
<slot />
<!-- default slot -->
<button @click="emit('edit', name)">Edit</button>
</div>
</template>
<!-- Parent -->
<UserCard name="Amit" @edit="handleEdit">
<template #avatar><img src="/avatar.png" /></template>
<p>Senior Developer</p>
</UserCard>30. Dynamic Components
Theory
<component :is="activeTab"> switches rendered component. Works with keep-alive.
<component :is="tabs[activeIndex].component" />31. Emits & Two-Way Communication
const emit = defineEmits(['update:modelValue', 'save'])
emit('update:modelValue', newValue)32. Keys in v-for
<li v-for="item in todos" :key="item.id">{{ item.text }}</li>33. Props & Emits
Theory
Props flow down (read-only). Emits flow up as events. Use defineProps and defineEmits in script setup.
<script setup>
const props = defineProps({ label: String })
const emit = defineEmits(['submit'])
</script>
<template>
<button @click="emit('submit')">{{ label }}</button>
</template>34. Props Validation
defineProps({
id: { type: String, required: true },
status: { type: String, default: 'active', validator: (v) => ['active', 'archived'].includes(v) },
})35. Slots
Theory
Default slot, named slots (#header), scoped slots (child passes data to slot).
<!-- Child -->
<slot name="header" :title="pageTitle" />
<slot :item="row" />
<!-- Parent -->
<template #header="{ title }">
<h1>{{ title }}</h1>
</template>
<template #default="{ item }">
<span>{{ item.name }}</span>
</template>36. Template Refs
<script setup>
const inputRef = ref(null)
onMounted(() => inputRef.value.focus())
</script>
<template><input ref="inputRef" /></template>37. Typed defineProps & defineEmits
const props = defineProps<{ id: string; optional?: number }>()
const emit = defineEmits<{ (e: 'save', payload: FormData): void }>()38. v-if vs v-show
Theory
v-if — conditional mount (lazy, higher toggle cost). v-show — CSS display toggle (initial render cost).
39. v-model
Theory
Two-way binding — :modelValue + @update:modelValue. Can use multiple v-models on one component.
<!-- Parent -->
<CustomInput v-model="search" />
<!-- Equivalent -->
<CustomInput :modelValue="search" @update:modelValue="search = $event" />
<!-- Multiple v-models -->
<UserForm v-model:name="name" v-model:email="email" />40. v-model on Components
State Management
41. Auth with Pinia + Router
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(null)
const isLoggedIn = computed(() => !!token.value)
async function login(credentials) {
const { data } = await api.post('/auth/login', credentials)
user.value = data.user
token.value = data.token
}
function logout() {
user.value = null
token.value = null
}
return { user, token, isLoggedIn, login, logout }
})42. Context vs Pinia Performance
| provide/inject | Pinia | |
|---|---|---|
| Re-render | All injectors on change | Store subscribers only |
| DevTools | No | Yes |
| Use | Theme, i18n | Cart, auth, complex state |
43. Pinia + API Layer
// services/api.js
export const api = axios.create({ baseURL: '/api/v1' })
// stores/products.js
export const useProductStore = defineStore('products', () => {
const products = ref([])
const loading = ref(false)
async function fetchProducts() {
loading.value = true
try {
products.value = (await api.get('/products')).data
} finally {
loading.value = false
}
}
return { products, loading, fetchProducts }
})44. Pinia Async Actions
async function fetchProducts() {
this.loading = true
try {
this.products = await api.getProducts()
} finally {
this.loading = false
}
}45. Pinia Basics
46. Pinia defineStore
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const total = computed(() => items.value.reduce((s, i) => s + i.price, 0))
function addItem(p) {
items.value.push(p)
}
return { items, total, addItem }
})47. Pinia State Management
Theory
Pinia is the official Vue 3 store — simpler than Vuex, no mutations, full TypeScript, DevTools support.
Concepts: defineStore → state, getters, actions.
Real Example
// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: {
total: (state) => state.items.reduce((sum, i) => sum + i.price, 0),
itemCount: (state) => state.items.length,
},
actions: {
addItem(product) {
this.items.push(product)
},
async checkout() {
await api.post('/checkout', this.items)
this.items = []
},
},
})
// Component
const cart = useCartStore()
cart.addItem(product)
console.log(cart.total)// Setup store syntax (Composition-style)
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials) {
user.value = await api.login(credentials)
}
return { user, isLoggedIn, login }
})48. Pinia vs Component State
49. Pinia vs Vuex
| Vuex 4 | Pinia | |
|---|---|---|
| Mutations | Required | Removed |
| Modules | Nested modules | Flat stores |
| TypeScript | Boilerplate | Native |
| DevTools | Yes | Yes |
| Vue 3 | Supported | Recommended |
// Vuex (legacy)
mutations: { ADD_ITEM(state, item) { state.items.push(item); } },
actions: { addItem({ commit }, item) { commit("ADD_ITEM", item); } },
// Pinia
actions: { addItem(item) { this.items.push(item); } },50. Pinia Workflow
User action → store.action() → state update → reactive subscribers re-renderconst cart = useCartStore()
cart.addItem(product) // action mutates state (Immer in Pinia)
// Components using storeToRefs(cart) re-render51. Store Composition
const user = useUserStore()
const cart = useCartStore()
const checkout = computed(() => ({ user: user.profile, items: cart.items }))52. Vuex to Pinia Migration
53. Vuex vs Pinia
| Vuex | Pinia | |
|---|---|---|
| Mutations | Required | Removed |
| Modules | Manual | Automatic per store |
| TypeScript | Verbose | Native |
| Vue 3 | Vuex 4 | Recommended |
Routing & Navigation
54. Lazy-Loaded Routes
{ path: "/admin", component: () => import("./pages/Admin.vue") }55. Navigation Guards
router.beforeEach(async (to) => {
if (to.meta.auth && !isLoggedIn()) return '/login'
})56. Route Configuration
{ path: "/products/:id", name: "product", component: Product, props: true }57. useRoute & useRouter
const route = useRoute()
const router = useRouter()
router.push({ name: 'product', params: { id: '123' } })58. Vue Router
Theory
Vue Router 4 handles client-side routing for Vue 3.
Key APIs: createRouter, RouterView, RouterLink, useRoute, useRouter, navigation guards.
Real Example
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: () => import('./pages/Home.vue') },
{
path: '/products/:id',
component: () => import('./pages/Product.vue'),
props: true,
},
{
path: '/dashboard',
component: () => import('./layouts/Dashboard.vue'),
meta: { requiresAuth: true },
children: [{ path: '', component: () => import('./pages/DashboardHome.vue') }],
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to, from, next) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) next('/login')
else next()
})59. Vue Router Basics
Theory
createRouter, RouterView, RouterLink, dynamic routes with :id, useRoute(), useRouter().
const routes = [{ path: '/user/:id', component: UserPage }]
const router = createRouter({ history: createWebHistory(), routes })API & Data Fetching
60. @tanstack/vue-query
const { data, isLoading } = useQuery({
queryKey: ['products', filters],
queryFn: () => api.getProducts(filters),
})61. Composition API
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useProductStore } from '@/stores/products'
const store = useProductStore()
const filter = ref('')
const filtered = computed(() => store.products.filter((p) => p.name.includes(filter.value)))
onMounted(() => store.fetchProducts())
</script>62. CORS
// Preflight for PUT with JSON
fetch('https://api.example.com/orders', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
})63. Long Polling vs WebSockets
| Long Polling | WebSocket | |
|---|---|---|
| Connection | Repeated HTTP | Persistent |
| Latency | Higher | Lower |
| Use | Fallback | Live tracking |
64. useAsyncData / useFetch (Nuxt)
const { data, pending } = await useFetch('/api/products')65. Vue Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
const { data, isLoading, error } = useQuery({
queryKey: ['products', category],
queryFn: () => api.getProducts(category.value),
})
const queryClient = useQueryClient()
const { mutate: addToCart } = useMutation({
mutationFn: (item) => api.addToCart(item),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cart'] }),
})Pinia vs Vue Query
| Pinia | Vue Query | |
|---|---|---|
| Data | Client state (UI, cart) | Server state (API cache) |
| Cache | Manual | Automatic |
| Refetch | Manual | Background, staleTime |
66. WebSockets
// composables/useWebSocket.js
export function useWebSocket(url) {
const data = ref(null)
const status = ref('connecting')
let ws
onMounted(() => {
ws = new WebSocket(url)
ws.onopen = () => (status.value = 'open')
ws.onmessage = (e) => (data.value = JSON.parse(e.data))
ws.onclose = () => (status.value = 'closed')
})
onUnmounted(() => ws?.close())
return { data, status }
}Performance
67. defineAsyncComponent
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: Spinner,
delay: 200,
})68. Keep-alive
Theory
Cache inactive component instances — preserve state in tab switches.
<keep-alive include="Dashboard,Settings">
<component :is="view" />
</keep-alive>69. Performance — v-memo
<div v-memo="[item.id, item.selected]">
<ExpensiveRow :item="item" />
</div>70. Performance Optimization
Theory
| Technique | Purpose |
|---|---|
v-memo | Skip re-render if deps unchanged |
keep-alive | Cache inactive components |
| Lazy routes | Code splitting |
defineAsyncComponent | Async component loading |
shallowRef / markRaw | Reduce reactivity overhead |
| Virtual scrolling | Large lists |
71. Suspense
Theory
Show fallback while async setup or async components resolve.
<Suspense>
<AsyncDashboard />
<template #fallback><Spinner /></template>
</Suspense>72. Teleport
Theory
Render content elsewhere in DOM — modals, toasts to <body>.
<Teleport to="body">
<div class="modal">...</div>
</Teleport>73. v-memo
74. Virtual Scrolling
Nuxt & SSR
75. CSR vs SSR
| Vue SPA (Vite) | Nuxt SSR | |
|---|---|---|
| Render | Client | Server + hydrate |
| SEO | Needs prerender | Built-in |
76. Hydration Mismatch
77. Nuxt Fullstack
// server/api/products.get.ts — Nuxt server route
export default defineEventHandler(async () => {
return await db.products.findMany()
})
// pages/products.vue
const { data: products } = await useFetch('/api/products')78. Nuxt.js & SSR
Theory
Nuxt 3 is the Vue meta-framework — file-based routing, SSR/SSG, auto-imports, server API routes.
Rendering: useFetch, useAsyncData for SSR-safe data fetching.
Architecture & Patterns
79. Feature-Based Structure
src/features/cart/{components,composables,stores,types}
src/shared/{components,composables}80. Plugins
app.use(router).use(pinia).use(i18n)81. render Functions & h()
82. Vue Architecture
src/features/{auth,catalog,cart}/
components/, composables/, stores/, api/
src/shared/
src/app/ — router, plugins, App.vue83. Vue Design Patterns
| Pattern | Vue equivalent |
|---|---|
| Custom hooks | Composables |
| Context | provide/inject, Pinia |
| HOC | Composables or wrapper components |
| Render props | Scoped slots |
| Compound components | Multi-component with provide/inject |
| Pattern | Q# | Core idea |
|---|---|---|
| Fundamentals | 1–5 | Progressive framework, SFC, directives |
| Components | 6–10 | Props down, emits up, v-model |
| Reactivity | 11–16 | ref, reactive, computed, watch |
| Composition | 17–22 | script setup, composables |
| State | 23–26 | Pinia stores |
| Router | 27–30 | Guards, lazy routes |
| Async | 31–34 | Composables, vue-query |
| Performance | 35–38 | v-memo, keep-alive, async |
| Advanced | 39–43 | Teleport, Suspense |
| Nuxt | 44–46 | SSR, useFetch |
| TS/Test | 47–48 | Typed props, Vitest |
| Architecture | 49–50 | Feature folders, patterns |
50 patterns. Master reactivity + Composition API + Pinia and Vue interviews become predictable.
JavaScript Core
84. Async
async function loadDashboard() {
const [user, orders] = await Promise.all([api.get('/user'), api.get('/orders')])
return { user: user.data, orders: orders.data }
}85. Closure
function createStore(initial) {
let state = initial
return {
get: () => state,
set: (v) => {
state = v
},
}
}86. Debouncing
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debounced = ref(value.value)
let timer
watch(value, (v) => {
clearTimeout(timer)
timer = setTimeout(() => {
debounced.value = v
}, delay)
})
return debounced
}<script setup>
const query = ref('')
const debouncedQuery = useDebounce(query, 400)
const { data } = useFetch(computed(() => (debouncedQuery.value ? `/api/search?q=${debouncedQuery.value}` : null)))
</script>| Topic | Vue approach |
|---|---|
| Counter | ref + composable |
| useFetch | watch + AbortController |
| Merge | reduce + spread / deepMerge |
| Debounce | useDebounce composable |
| Share state | provide/inject or Pinia |
87. Debouncing & Throttling
// Vue composable
export function useDebounce(value, delay = 300) {
const debounced = ref(value.value)
watch(value, (v) => {
const t = setTimeout(() => (debounced.value = v), delay)
onScopeDispose(() => clearTimeout(t))
})
return debounced
}88. Event Loop
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 1 → 4 → 3 → 289. Garbage Collection
TypeScript
90. TypeScript
<script setup lang="ts">
interface Product {
id: string
name: string
price: number
}
const props = withDefaults(
defineProps<{
product: Product
editable?: boolean
}>(),
{ editable: false },
)
const emit = defineEmits<{
save: [product: Product]
cancel: []
}>()
</script>// Typed store
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const isAdmin = computed(() => user.value?.role === 'admin')
return { user, isAdmin }
})91. TypeScript with Vue
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = withDefaults(defineProps<Props>(), { count: 0 })
const emit = defineEmits<{ submit: [id: string] }>()
</script>Security & Accessibility
92. Accessibility
<nav aria-label="Main">
<button type="button" aria-pressed="true">Active</button>
</nav>Testing & Tooling
93. Vitest + Vue Test Utils
import { mount } from '@vue/test-utils'
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')Behavioral & Soft Skills
94. Behavioral Questions
Use STAR method: Situation → Task → Action → Result.
- Previous project: What you built, stack (Vue 3, Pinia, Nuxt), measurable impact
- Problem solving: Reproduce → DevTools → fix → verify
- Collaboration: Code review, disagreeing on Pinia vs provide/inject
| Round | Topic | One-liner |
|---|---|---|
| 1 | CORS | Server Allow-Origin header |
| 1 | WebSocket | Persistent duplex — use composable + onUnmounted cleanup |
| 1 | Event loop | Sync → micro → macro |
| 2 | Grid toggle | ref 2D array, immutable map toggle |
| 3 | Culture | STAR stories with metrics |
General & Advanced
95. Auth
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (to.meta.auth && !auth.token) return '/login'
})96. Batching
count.value++
flag.value = true
// One re-render
await nextTick()
// DOM updated97. Browser Storage
| API | Lifetime | Use |
|---|---|---|
| localStorage | Persistent | Preferences |
| sessionStorage | Tab | Wizard state |
| Cookies | Configurable | httpOnly auth |
| IndexedDB | Persistent | Offline cart |
98. Computed Properties
Theory
Cached derived values — only recompute when dependencies change. Getter-only by default; setter optional.
const firstName = ref('Amit')
const lastName = ref('Shah')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)99. Conditional Rendering
Theory
v-if / v-else-if / v-else, v-show, <template v-if> for grouping without extra DOM.
Real Example
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<ProductList v-else :items="products" />
</template>100. Counter
Basic
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="decrement">−</button>
<button @click="increment">+</button>
</div>
</template>Senior — Composable + Bounds
// composables/useCounter.js
export function useCounter(initial = 0, { min = -Infinity, max = Infinity, step = 1 } = {}) {
const count = ref(initial)
const increment = () => {
count.value = Math.min(count.value + step, max)
}
const decrement = () => {
count.value = Math.max(count.value - step, min)
}
const reset = () => {
count.value = initial
}
return { count, increment, decrement, reset }
}<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, increment, decrement, reset } = useCounter(0, { min: 0, max: 10 })
</script>
<template>
<output aria-live="polite">{{ count }}</output>
<button :disabled="count <= 0" @click="decrement">−</button>
<button :disabled="count >= 10" @click="increment">+</button>
<button @click="reset">Reset</button>
</template>101. Decision Guide
| Need | Tool |
|---|---|
| Cart, theme, auth UI state | Pinia |
| API product list | Vue Query |
| Form input | ref/reactive local |
| URL filters | Vue Router query params |
| Deep tree config | provide/inject |
102. Deep vs Shallow Copy
const deep = structuredClone(original)
const shallow = { ...original }103. ES6
Spread, destructuring, optional chaining, nullish coalescing, modules, arrow functions.
104. Event Handling & Modifiers
<form @submit.prevent="handleSubmit">
<input @keyup.enter="search" />
<a @click.stop.prevent="navigate">Link</a>
</form>105. Flatten without Built-ins
function flatten(arr) {
const result = []
for (const item of arr) {
if (Array.isArray(item)) result.push(...flatten(item))
else result.push(item)
}
return result
}| Topic | Vue angle |
|---|---|
| State workflow | Pinia actions, not Redux dispatch |
| Rules | Composables at top level — like hooks |
| SSR | Nuxt, not Next.js |
| Batching | Vue reactive batch + nextTick |
106. Flexbox vs Grid
107. Grid Toggle Component
Requirements
- N×N grid, click toggles cell on/off
- Immutable state updates
- Clean component structure
<!-- GridToggle.vue -->
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
rows: { type: Number, default: 3 },
cols: { type: Number, default: 3 },
})
function createGrid(r, c) {
return Array.from({ length: r }, () => Array(c).fill(false))
}
const grid = ref(createGrid(props.rows, props.cols))
function toggleCell(row, col) {
grid.value = grid.value.map((r, ri) => (ri === row ? r.map((cell, ci) => (ci === col ? !cell : cell)) : r))
}
function reset() {
grid.value = createGrid(props.rows, props.cols)
}
const activeCount = computed(() => grid.value.flat().filter(Boolean).length)
</script>
<template>
<div class="grid-toggle">
<header>
<span>{{ activeCount }} / {{ rows * cols }} active</span>
<button type="button" @click="reset">Reset</button>
</header>
<div role="grid" :aria-rowcount="rows" :aria-colcount="cols">
<div v-for="(row, rowIndex) in grid" :key="rowIndex" role="row" class="grid-row">
<button
v-for="(isActive, colIndex) in row"
:key="colIndex"
type="button"
role="gridcell"
:aria-pressed="isActive"
:aria-label="`Row ${rowIndex + 1}, Col ${colIndex + 1}, ${isActive ? 'on' : 'off'}`"
:class="['cell', { 'cell--active': isActive }]"
@click="toggleCell(rowIndex, colIndex)" />
</div>
</div>
</div>
</template>
<style scoped>
.grid-row {
display: flex;
gap: 4px;
margin-bottom: 4px;
}
.cell {
cursor: pointer;
border: 2px solid #374151;
border-radius: 6px;
background: #f9fafb;
width: 48px;
height: 48px;
}
.cell--active {
border-color: #16a34a;
background: #22c55e;
}
</style>Composable Version
// composables/useGridToggle.js
export function useGridToggle(rows, cols) {
const grid = ref(createGrid(rows, cols))
const toggleCell = (row, col) => {
grid.value = grid.value.map((r, ri) => (ri === row ? r.map((c, ci) => (ci === col ? !c : c)) : r))
}
const activeCount = computed(() => grid.value.flat().filter(Boolean).length)
return { grid, toggleCell, activeCount }
}Immutable Update — Explain in Interview
// ❌ Mutates — Vue may not detect if same reference
grid.value[row][col] = !grid.value[row][col]
// ✅ New references — always triggers update
grid.value = grid.value.map(/* ... */)108. JavaScript Essentials
Closure: Function + remembered scope — used in composables and debounce.
Event loop: Sync → microtasks (Promise) → macrotask (setTimeout).
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4) // 1,4,3,2109. Lifecycle Hooks
Theory
| Options API | Composition API | When |
|---|---|---|
beforeCreate | setup() | Before instance |
created | setup() | After reactive setup |
beforeMount | onBeforeMount | Before DOM mount |
mounted | onMounted | DOM ready |
beforeUpdate | onBeforeUpdate | Before re-render |
updated | onUpdated | After re-render |
beforeUnmount | onBeforeUnmount | Before destroy |
unmounted | onUnmounted | Cleanup |
110. Lifecycle Mapping
| Options | Composition |
|---|---|
| mounted | onMounted |
| unmounted | onUnmounted |
111. Loading / Error / Empty UI
<template>
<Spinner v-if="loading" />
<ErrorBanner v-else-if="error" @retry="refetch" />
<EmptyState v-else-if="!items.length" />
<ItemList v-else :items="items" />
</template>112. Merge Objects
// Shallow merge — later wins
function mergeObjects(list) {
return list.reduce((acc, obj) => ({ ...acc, ...obj }), {})
}
// Deep merge
function deepMerge(target, source) {
const result = { ...target }
for (const key of Object.keys(source)) {
if (isPlainObject(target[key]) && isPlainObject(source[key])) {
result[key] = deepMerge(target[key], source[key])
} else {
result[key] = source[key]
}
}
return result
}
function isPlainObject(v) {
return v !== null && typeof v === 'object' && !Array.isArray(v)
}
function mergeObjectList(list) {
return list.reduce((acc, obj) => deepMerge(acc, obj), {})
}<script setup>
import { computed } from 'vue'
const configSources = ref([
{ theme: 'dark', api: '/v1' },
{ api: '/v2', timeout: 5000 },
])
const mergedConfig = computed(() => configSources.value.reduce((acc, c) => ({ ...acc, ...c }), {}))
</script>113. Missing Numbers
function findMissing(arr) {
const set = new Set(arr)
const missing = []
for (let i = Math.min(...arr); i <= Math.max(...arr); i++) {
if (!set.has(i)) missing.push(i)
}
return missing
}
findMissing([1, 2, 3, 5, 7]) // [4, 6]114. Provide / Inject
Theory
provide/inject passes data from ancestor to descendant without prop drilling. Symbol keys recommended for large apps.
Unlike props, inject is not explicitly declared in every middle component.
Real Example
// provider.ts
import { provide, inject, readonly, ref } from 'vue'
const ThemeKey = Symbol('theme')
export function provideTheme() {
const theme = ref('light')
const toggle = () => (theme.value = theme.value === 'light' ? 'dark' : 'light')
provide(ThemeKey, { theme: readonly(theme), toggle })
}
export function useTheme() {
const ctx = inject(ThemeKey)
if (!ctx) throw new Error('useTheme requires provider')
return ctx
}115. Realtime
const { data: orderStatus } = useWebSocket(`wss://api.example.com/orders/${id}`)| Topic | Answer |
|---|---|
| Vue fullstack | Vue/Nuxt frontend + REST/GraphQL + optional Nuxt server routes |
| State | Pinia for client, useFetch/vue-query for server |
| Auth | JWT in httpOnly cookie, router guards, Pinia auth store |
116. Render vs Patch
117. Star Rating (Vue)
<script setup>
const props = defineProps({ max: { type: Number, default: 10 }, modelValue: Number })
const emit = defineEmits(['update:modelValue'])
const hover = ref(0)
const display = computed(() => hover.value || props.modelValue || 0)
</script>
<template>
<div role="radiogroup" :aria-label="`Rating out of ${max}`">
<button
v-for="n in max"
:key="n"
type="button"
role="radio"
:aria-checked="modelValue === n"
:aria-label="`${n} out of ${max}`"
@click="emit('update:modelValue', n)"
@mouseenter="hover = n"
@mouseleave="hover = 0"
:style="{ color: n <= display ? '#f59e0b' : '#d1d5db' }">
★
</button>
</div>
</template>118. Store Styles
// Options store
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: { total: (s) => s.items.reduce((a, i) => a + i.price, 0) },
actions: {
addItem(p) {
this.items.push(p)
},
},
})
// Setup store (Composition — like script setup)
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const total = computed(() => items.value.reduce((a, i) => a + i.price, 0))
function addItem(p) {
items.value.push(p)
}
return { items, total, addItem }
})119. Vue 3.5+
- useTemplateRef() — typed template refs
- Reactive Props Destructure — destructure props reactively
- SSR improvements in Nuxt 3
<script setup>
const props = defineProps<{ count: number }>();
// Vue 3.5: count stays reactive when destructured
const { count } = props;
</script>| Topic | One-liner |
|---|---|
| script setup | Macros, auto-expose, less boilerplate |
| shallowRef | Large objects — manual triggerRef |
| Auth | Pinia store + router.beforeEach guard |
| Performance | Pinia over provide for hot state |
120. Vue Best Practices
- Default to
<script setup>+ Composition API - Colocate composables per feature
- Pinia for global state, not provide/inject for everything
- Lazy-load routes and heavy components
- Use TypeScript for props and emits
- Scoped styles; CSS modules for complex components
| # | Topic | One-liner |
|---|---|---|
| 1 | Vue | Progressive reactive framework |
| 6 | ref/reactive | .value vs Proxy object |
| 5 | v-model | modelValue + update:modelValue |
| 11 | watch | Explicit source, old/new value |
| 17 | Pinia | defineStore, no mutations |
| 26 | Pinia vs Vuex | Pinia for Vue 3 |
| 15 | Composables | Reusable setup logic |
121. watch vs onMounted Order
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime