All topics
Frontend · Learning hub

Vue notes for developers

Master Vue with a curated set of 8 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Vue

Reactivity System & Composition API

Vue Reactivity System & Composition API Vue 3 introduced the Composition API, a powerful set of APIs that allows you to author Vue components using imported fun

Vue Reactivity System & Composition API

Vue 3 introduced the Composition API, a powerful set of APIs that allows you to author Vue components using imported functions. Understanding Vue's reactivity system is key to building efficient applications.

The Reactivity System

Vue's reactivity system automatically tracks dependencies and updates the DOM when data changes. It uses Proxy objects to intercept property access and modifications.

ref() - Reactive References

ref() creates a reactive reference to a value. It works with primitives and objects, and requires `.value` to access the underlying value in JavaScript (but not in templates).

<script setup>
import { ref } from 'vue'

// Primitive values
const count = ref(0)
const message = ref('Hello Vue')
const isActive = ref(false)

// Accessing values
console.log(count.value) // 0
count.value++ // increment

// Arrays and objects
const todos = ref([
  { id: 1, text: 'Learn Vue', done: false },
  { id: 2, text: 'Build app', done: false },
])

const user = ref({
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
})

// Updating nested properties
user.value.age = 31
todos.value[0].done = true

// Adding to array
todos.value.push({ id: 3, text: 'Deploy', done: false })
</script>

<template>
  <div>
    <!-- In templates, .value is not needed -->
    <h1>{{ message }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="count++">Increment</button>
    
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done" />
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

reactive() - Reactive Objects

reactive() creates a reactive proxy for objects. Unlike ref(), it doesn't require `.value` and provides a more natural API for working with objects.

<script setup>
import { reactive } from 'vue'

// Create reactive object
const state = reactive({
  count: 0,
  message: 'Hello',
  user: {
    name: 'John',
    email: 'john@example.com',
  },
  todos: [],
})

// Direct property access (no .value needed)
state.count++
state.message = 'Hello Vue'
state.user.name = 'Jane'
state.todos.push({ id: 1, text: 'Learn Vue' })

// Destructuring loses reactivity - use toRefs
import { toRefs } from 'vue'
const { count, message } = toRefs(state) // Now count and message are refs

// Or use toRef for single property
import { toRef } from 'vue'
const count = toRef(state, 'count')
</script>

<template>
  <div>
    <h1>{{ state.message }}</h1>
    <p>Count: {{ state.count }}</p>
    <button @click="state.count++">Increment</button>
  </div>
</template>

computed() - Computed Properties

Computed properties are cached reactive values that automatically update when their dependencies change. They are ideal for deriving state from existing reactive data.

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// Read-only computed
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// Writable computed
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const [first, last] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  },
})

// Complex computed with filtering and sorting
const todos = ref([
  { id: 1, text: 'Learn Vue', done: true, priority: 'high' },
  { id: 2, text: 'Build app', done: false, priority: 'high' },
  { id: 3, text: 'Deploy', done: false, priority: 'low' },
])

const activeTodos = computed(() => {
  return todos.value.filter(todo => !todo.done)
})

const highPriorityTodos = computed(() => {
  return todos.value
    .filter(todo => todo.priority === 'high')
    .sort((a, b) => a.text.localeCompare(b.text))
})

const todoStats = computed(() => ({
  total: todos.value.length,
  completed: todos.value.filter(t => t.done).length,
  active: todos.value.filter(t => !t.done).length,
  completionRate: (todos.value.filter(t => t.done).length / todos.value.length * 100).toFixed(1),
}))
</script>

<template>
  <div>
    <h1>{{ fullName }}</h1>
    <input v-model="fullNameWritable" placeholder="Enter full name" />
    
    <div>
      <p>Total: {{ todoStats.total }}</p>
      <p>Completed: {{ todoStats.completed }}</p>
      <p>Active: {{ todoStats.active }}</p>
      <p>Completion Rate: {{ todoStats.completionRate }}%</p>
    </div>
  </div>
</template>

watch() & watchEffect()

Watchers allow you to perform side effects in response to reactive state changes. watch() is explicit about what to watch, while watchEffect() automatically tracks dependencies.

<script setup>
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const searchQuery = ref('')
const user = ref({ name: 'John', age: 30 })

// watch() - explicit dependencies
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`)
})

// Watch multiple sources
watch([count, searchQuery], ([newCount, newQuery], [oldCount, oldQuery]) => {
  console.log('Count or query changed')
})

// Watch with immediate option
watch(
  searchQuery,
  async (newQuery) => {
    if (newQuery.length > 2) {
      const results = await searchAPI(newQuery)
      console.log('Search results:', results)
    }
  },
  { immediate: true } // Run on mount
)

// Deep watch for nested objects
watch(
  user,
  (newUser) => {
    console.log('User changed:', newUser)
  },
  { deep: true } // Watch nested properties
)

// watchEffect() - automatic dependency tracking
watchEffect(() => {
  // Automatically tracks count and searchQuery
  console.log(`Search '${searchQuery.value}' with count ${count.value}`)
})

// watchEffect with cleanup
watchEffect((onCleanup) => {
  const controller = new AbortController()
  
  fetch(`/api/search?q=${searchQuery.value}`, {
    signal: controller.signal,
  })
    .then(res => res.json())
    .then(data => console.log(data))
  
  onCleanup(() => {
    // Cancel pending requests
    controller.abort()
  })
})

// Stop watching
const stop = watch(count, () => {})
stop() // Stop watching
</script>

Composition API Fundamentals

setup() Function

The setup() function is the entry point for using the Composition API. The <script setup> syntax is syntactic sugar that makes it more ergonomic.

// Traditional setup() function
export default {
  setup(props, context) {
    const count = ref(0)
    const increment = () => count.value++
    
    // Must explicitly return what to expose to template
    return {
      count,
      increment,
    }
  },
}

// <script setup> - recommended
<script setup>
import { ref } from 'vue'

// Everything is automatically exposed to template
const count = ref(0)
const increment = () => count.value++

// Props
const props = defineProps({
  title: String,
  initialCount: { type: Number, default: 0 },
})

// Emits
const emit = defineEmits(['update', 'delete'])

const handleUpdate = () => {
  emit('update', count.value)
}
</script>

<template>
  <div>
    <h2>{{ props.title }}</h2>
    <p>Count: {{ count }}</p>
    <button @click="increment">+</button>
    <button @click="handleUpdate">Update Parent</button>
  </div>
</template>

Lifecycle Hooks

Vue provides lifecycle hooks that allow you to run code at specific stages of a component's lifecycle.

<script setup>
import {
  ref,
  onMounted,
  onUpdated,
  onUnmounted,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount,
} from 'vue'

const data = ref(null)
const timer = ref(null)

// Before component is mounted
onBeforeMount(() => {
  console.log('Component is about to mount')
})

// After component is mounted
onMounted(() => {
  console.log('Component mounted')
  
  // Fetch data
  fetch('/api/data')
    .then(res => res.json())
    .then(result => {
      data.value = result
    })
  
  // Start timer
  timer.value = setInterval(() => {
    console.log('Timer tick')
  }, 1000)
})

// After component updates
onUpdated(() => {
  console.log('Component updated')
})

// Before component unmounts
onBeforeUnmount(() => {
  console.log('Component about to unmount')
})

// After component unmounts
onUnmounted(() => {
  console.log('Component unmounted')
  
  // Cleanup
  if (timer.value) {
    clearInterval(timer.value)
  }
})
</script>

Composables - Reusable Logic

Composables are functions that leverage Vue's Composition API to encapsulate and reuse stateful logic. They are similar to React custom hooks.

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}

// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  watchEffect(async () => {
    // toValue() normalizes refs and plain values
    const urlValue = toValue(url)
    
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(urlValue)
      if (!response.ok) throw new Error(response.statusText)
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })
  
  return { data, error, loading }
}

// composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const value = ref(stored ? JSON.parse(stored) : defaultValue)
  
  watch(
    value,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue))
    },
    { deep: true }
  )
  
  return value
}

// Usage in component
<script setup>
import { useMouse } from './composables/useMouse'
import { useFetch } from './composables/useFetch'
import { useLocalStorage } from './composables/useLocalStorage'

const { x, y } = useMouse()
const userId = ref(1)
const { data: user, loading, error } = useFetch(() => `/api/users/${userId.value}`)
const theme = useLocalStorage('theme', 'light')
</script>

<template>
  <div>
    <p>Mouse: {{ x }}, {{ y }}</p>
    <p v-if="loading">Loading...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <div v-else>User: {{ user?.name }}</div>
    <button @click="theme = theme === 'light' ? 'dark' : 'light'">
      Toggle Theme ({{ theme }})
    </button>
  </div>
</template>

Reactive Utilities

shallowRef() & shallowReactive()

For performance optimization, use shallow variants when you only need reactivity at the root level.

import { shallowRef, shallowReactive, triggerRef } from 'vue'

// shallowRef - only .value is reactive
const state = shallowRef({ count: 0 })

state.value.count++ // Won't trigger update
state.value = { count: 1 } // Will trigger update

// Force trigger update
triggerRef(state)

// shallowReactive - only root-level properties are reactive
const stateReactive = shallowReactive({
  count: 0,
  nested: { value: 0 },
})

stateReactive.count++ // Will trigger update
stateReactive.nested.value++ // Won't trigger update
stateReactive.nested = { value: 1 } // Will trigger update

readonly() & isRef()

import { ref, readonly, isRef, isReactive } from 'vue'

const original = ref(0)
const copy = readonly(original)

copy.value++ // Warning: cannot modify readonly ref
original.value++ // OK, and copy reflects the change

// Type checking
const count = ref(0)
const state = reactive({})

console.log(isRef(count)) // true
console.log(isRef(state)) // false
console.log(isReactive(state)) // true
console.log(isReactive(count)) // false

Best Practices

  • Use ref() for primitives, reactive() for objects

  • Prefer computed() over watch() when deriving values

  • Extract reusable logic into composables

  • Use <script setup> for cleaner syntax

  • Clean up side effects in onUnmounted()

Vue

Component Communication & Props

Vue Component Communication & Props Learn how Vue components communicate with each other through props, events, provide/inject, and other patterns: Props - Pare

Vue Component Communication & Props

Learn how Vue components communicate with each other through props, events, provide/inject, and other patterns:

Props - Parent to Child Communication

Props allow parent components to pass data down to child components. They are reactive and can be validated with types and validators.

<script setup>
// Basic props
const props = defineProps({
  title: String,
  count: Number,
  user: Object,
  tags: Array,
})

// Props with defaults and validation
const props = defineProps({
  title: {
    type: String,
    required: true,
  },
  count: {
    type: Number,
    default: 0,
  },
  status: {
    type: String,
    default: 'pending',
    validator: (value) => ['pending', 'active', 'completed'].includes(value),
  },
  user: {
    type: Object,
    required: true,
    validator: (value) => value.id && value.name,
  },
})

// TypeScript props
interface Props {
  title: string
  count?: number
  user: { id: number; name: string }
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
})

// Destructuring props (loses reactivity)
const { title, count } = props // ❌ Not reactive

// Use toRefs to maintain reactivity
import { toRefs } from 'vue'
const { title, count } = toRefs(props) // ✅ Reactive
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <p>User: {{ user.name }}</p>
  </div>
</template>

Events - Child to Parent Communication

Components emit custom events to communicate with their parent. defineEmits() is used to declare which events a component can emit.

<!-- Child Component -->
<script setup>
const emit = defineEmits(['update', 'delete', 'statusChange'])

// With TypeScript
const emit = defineEmits<{
  update: [value: number]
  delete: [id: string]
  statusChange: [status: 'pending' | 'active' | 'completed']
}>()

const handleClick = () => {
  emit('update', 42)
}

const handleDelete = (id) => {
  emit('delete', id)
}

const handleStatusChange = () => {
  emit('statusChange', 'completed')
}
</script>

<!-- Parent Component -->
<script setup>
import ChildComponent from './ChildComponent.vue'

const handleUpdate = (value) => {
  console.log('Received:', value)
}

const handleDelete = (id) => {
  console.log('Delete:', id)
}
</script>

<template>
  <ChildComponent
    @update="handleUpdate"
    @delete="handleDelete"
    @status-change="(status) => console.log('Status:', status)"
  />
</template>

v-model - Two-Way Binding

v-model provides two-way data binding between parent and child components. It's syntactic sugar for passing a prop and listening to an event.

<!-- Custom Input Component -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const updateValue = (event) => {
  emit('update:modelValue', event.target.value)
}
</script>

<template>
  <input
    :value="modelValue"
    @input="updateValue"
    placeholder="Enter text"
  />
</template>

<!-- Parent Usage -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const searchQuery = ref('')
</script>

<template>
  <!-- v-model is shorthand for :modelValue and @update:modelValue -->
  <CustomInput v-model="searchQuery" />
  <p>You typed: {{ searchQuery }}</p>
</template>

<!-- Multiple v-models -->
<script setup>
// Child component
const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])
</script>

<template>
  <div>
    <input
      :value="title"
      @input="$emit('update:title', $event.target.value)"
    />
    <textarea
      :value="content"
      @input="$emit('update:content', $event.target.value)"
    />
  </div>
</template>

<!-- Parent usage -->
<script setup>
const title = ref('')
const content = ref('')
</script>

<template>
  <MyComponent
    v-model:title="title"
    v-model:content="content"
  />
</template>

provide() / inject() - Dependency Injection

provide() and inject() enable passing data from ancestor to descendant components without prop drilling.

<!-- Root/Ancestor Component -->
<script setup>
import { ref, provide, readonly } from 'vue'

const theme = ref('light')
const user = ref({ name: 'John', role: 'admin' })

// Provide reactive values
provide('theme', theme)
provide('user', readonly(user)) // Readonly to prevent modification

// Provide methods
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)

// Provide with symbol keys (recommended for libraries)
const ThemeSymbol = Symbol()
provide(ThemeSymbol, theme)
</script>

<!-- Descendant Component (any level deep) -->
<script setup>
import { inject } from 'vue'

// Inject with default value
const theme = inject('theme', 'light')
const user = inject('user')
const toggleTheme = inject('toggleTheme')

// Inject with symbol key
const ThemeSymbol = Symbol()
const themeFromSymbol = inject(ThemeSymbol)

// Type-safe injection with TypeScript
interface User {
  name: string
  role: string
}

const user = inject<User>('user')
const theme = inject<Ref<string>>('theme', ref('light'))
</script>

<template>
  <div :class="theme">
    <p>Current user: {{ user?.name }}</p>
    <button @click="toggleTheme">Toggle Theme</button>
  </div>
</template>

Slots - Content Distribution

Slots allow parent components to pass template content to child components, enabling flexible component composition.

<!-- Card Component with slots -->
<template>
  <div class="card">
    <header v-if="$slots.header" class="card-header">
      <slot name="header" />
    </header>
    
    <div class="card-body">
      <!-- Default slot -->
      <slot />
    </div>
    
    <footer v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

<!-- Usage -->
<template>
  <Card>
    <template #header>
      <h2>Card Title</h2>
      <button>×</button>
    </template>
    
    <!-- Default slot content -->
    <p>This is the main content of the card.</p>
    <p>It can contain multiple elements.</p>
    
    <template #footer>
      <button>Cancel</button>
      <button>Save</button>
    </template>
  </Card>
</template>

<!-- Scoped Slots - Pass data to parent -->
<script setup>
const items = ref([
  { id: 1, name: 'Item 1', price: 100 },
  { id: 2, name: 'Item 2', price: 200 },
])
</script>

<template>
  <div>
    <!-- Child exposes data through slot props -->
    <div v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        <!-- Fallback content -->
        {{ item.name }}
      </slot>
    </div>
  </div>
</template>

<!-- Parent receives and uses slot props -->
<template>
  <ItemList>
    <template #default="{ item, index }">
      <div>
        <span>{{ index + 1 }}.</span>
        <strong>{{ item.name }}</strong>
        <span>${{ item.price }}</span>
      </div>
    </template>
  </ItemList>
</template>
Vue

Vue Router & Navigation

Vue Router & Navigation Vue Router is the official routing library for Vue.js. It deeply integrates with Vue core to make building Single Page Applications with

Vue Router & Navigation

Vue Router is the official routing library for Vue.js. It deeply integrates with Vue core to make building Single Page Applications with Vue a breeze.

Router Setup

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // Lazy-loaded route
    component: () => import('../views/About.vue'),
  },
  {
    path: '/users/:id',
    name: 'UserProfile',
    component: () => import('../views/UserProfile.vue'),
    props: true, // Pass route params as props
  },
  {
    path: '/dashboard',
    component: () => import('../views/Dashboard.vue'),
    children: [
      {
        path: '',
        name: 'DashboardHome',
        component: () => import('../views/DashboardHome.vue'),
      },
      {
        path: 'settings',
        name: 'Settings',
        component: () => import('../views/Settings.vue'),
      },
    ],
  },
  {
    // 404 catch-all
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    }
    return { top: 0 }
  },
})

export default router

Navigation in Components

<script setup>
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

// Access route params
const userId = route.params.id
const query = route.query.search

// Programmatic navigation
const goToUser = (id) => {
  router.push(`/users/${id}`)
}

const goBack = () => {
  router.back()
}

const navigateWithQuery = () => {
  router.push({
    name: 'UserProfile',
    params: { id: 123 },
    query: { tab: 'posts' },
  })
}

// Replace instead of push (no history entry)
const replaceRoute = () => {
  router.replace('/new-location')
}
</script>

<template>
  <div>
    <!-- Declarative navigation -->
    <router-link to="/">Home</router-link>
    <router-link :to="{ name: 'About' }">About</router-link>
    <router-link :to="`/users/${userId}`">User Profile</router-link>
    
    <!-- Active link styling -->
    <router-link
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      Dashboard
    </router-link>
    
    <!-- Router view -->
    <router-view v-slot="{ Component }">
      <transition name="fade" mode="out-in">
        <component :is="Component" />
      </transition>
    </router-view>
  </div>
</template>

Navigation Guards

Navigation guards allow you to protect routes, redirect users, or perform actions before/after navigation.

// Global before guard
router.beforeEach((to, from, next) => {
  // Check authentication
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

// Global after guard
router.afterEach((to, from) => {
  // Analytics tracking
  trackPageView(to.fullPath)
})

// Per-route guard
const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from) => {
      if (!isAdmin()) {
        return '/'
      }
    },
  },
]

// In-component guards
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// Prevent leaving with unsaved changes
const hasUnsavedChanges = ref(false)

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('You have unsaved changes. Leave anyway?')
    if (!answer) return false
  }
})

// React to param changes in same route
onBeforeRouteUpdate((to, from) => {
  // Fetch new user data when userId param changes
  if (to.params.id !== from.params.id) {
    fetchUser(to.params.id)
  }
})
</script>

Dynamic Routes & Nested Routes

const routes = [
  // Dynamic segments
  {
    path: '/users/:id',
    component: User,
  },
  {
    path: '/posts/:id(\\d+)', // Regex: only numbers
    component: Post,
  },
  {
    // Optional params
    path: '/search/:query?',
    component: Search,
  },
  {
    // Multiple params
    path: '/articles/:category/:slug',
    component: Article,
  },
  {
    // Catch-all route
    path: '/:pathMatch(.*)*',
    component: NotFound,
  },
  {
    // Nested routes
    path: '/dashboard',
    component: Dashboard,
    children: [
      {
        path: '', // Default child route
        component: DashboardHome,
      },
      {
        path: 'profile',
        component: Profile,
      },
      {
        path: 'settings',
        component: Settings,
        children: [
          {
            path: 'account',
            component: AccountSettings,
          },
          {
            path: 'privacy',
            component: PrivacySettings,
          },
        ],
      },
    ],
  },
]

Route Meta Fields & Lazy Loading

const routes = [
  {
    path: '/admin',
    component: Admin,
    meta: {
      requiresAuth: true,
      role: 'admin',
      title: 'Admin Panel',
    },
  },
  {
    path: '/public',
    component: Public,
    meta: {
      requiresAuth: false,
      layout: 'public',
    },
  },
]

// Access meta in components
<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()

// Update page title based on meta
watch(
  () => route.meta.title,
  (title) => {
    document.title = title || 'My App'
  },
  { immediate: true }
)
</script>

// Lazy loading with named chunks
const routes = [
  {
    path: '/heavy',
    component: () => import(/* webpackChunkName: "heavy" */ '../views/Heavy.vue'),
  },
]
Vue

State Management with Pinia

Vue State Management with Pinia Pinia is the official state management library for Vue. It provides a simple, type-safe API and works seamlessly with the Compos

Vue State Management with Pinia

Pinia is the official state management library for Vue. It provides a simple, type-safe API and works seamlessly with the Composition API and Vue DevTools.

Setting Up Pinia

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

Defining a Store

Stores in Pinia are defined using the defineStore() function. There are two syntaxes: Option Store (similar to Vue Options API) and Setup Store (similar to Composition API).

// stores/counter.js - Option Store syntax
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter',
  }),
  
  getters: {
    doubleCount: (state) => state.count * 2,
    
    // Accessing other getters
    isEven() {
      return this.count % 2 === 0
    },
    
    // Getter with parameters
    countPlusN: (state) => (n) => state.count + n,
  },
  
  actions: {
    increment() {
      this.count++
    },
    
    async incrementAsync() {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      this.count++
    },
    
    reset() {
      this.count = 0
    },
  },
})

// stores/todos.js - Setup Store syntax (recommended)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodoStore = defineStore('todos', () => {
  // State
  const todos = ref([])
  const filter = ref('all')
  const loading = ref(false)
  
  // Getters
  const completedTodos = computed(() =>
    todos.value.filter((todo) => todo.completed)
  )
  
  const activeTodos = computed(() =>
    todos.value.filter((todo) => !todo.completed)
  )
  
  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'completed':
        return completedTodos.value
      case 'active':
        return activeTodos.value
      default:
        return todos.value
    }
  })
  
  // Actions
  async function fetchTodos() {
    loading.value = true
    try {
      const response = await fetch('/api/todos')
      todos.value = await response.json()
    } finally {
      loading.value = false
    }
  }
  
  function addTodo(text) {
    todos.value.push({
      id: Date.now(),
      text,
      completed: false,
    })
  }
  
  function toggleTodo(id) {
    const todo = todos.value.find((t) => t.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  function deleteTodo(id) {
    const index = todos.value.findIndex((t) => t.id === id)
    if (index > -1) {
      todos.value.splice(index, 1)
    }
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  return {
    // State
    todos,
    filter,
    loading,
    // Getters
    completedTodos,
    activeTodos,
    filteredTodos,
    // Actions
    fetchTodos,
    addTodo,
    toggleTodo,
    deleteTodo,
    setFilter,
  }
})

Using Stores in Components

<script setup>
import { useTodoStore } from '@/stores/todos'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const todoStore = useTodoStore()
const counterStore = useCounterStore()

// Direct access to state and getters
console.log(todoStore.todos)
console.log(todoStore.filteredTodos)

// Call actions
todoStore.addTodo('Learn Pinia')
todoStore.toggleTodo(1)

// Destructuring (loses reactivity)
const { todos, filter } = todoStore // ❌ Not reactive

// Use storeToRefs to maintain reactivity
const { todos, filter, completedTodos } = storeToRefs(todoStore) // ✅ Reactive
const { addTodo, toggleTodo } = todoStore // Actions don't need storeToRefs

// Subscribe to state changes
todoStore.$subscribe((mutation, state) => {
  console.log('Store mutated:', mutation.type)
  console.log('New state:', state)
  
  // Persist to localStorage
  localStorage.setItem('todos', JSON.stringify(state.todos))
})

// Watch specific state
watch(
  () => todoStore.todos.length,
  (newLength) => {
    console.log(`Todos count: ${newLength}`)
  }
)
</script>

<template>
  <div>
    <h1>Todos ({{ todos.length }})</h1>
    
    <div>
      <button @click="() => setFilter('all')">All</button>
      <button @click="() => setFilter('active')">Active</button>
      <button @click="() => setFilter('completed')">Completed</button>
    </div>
    
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.completed"
          @change="() => toggleTodo(todo.id)"
        />
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

Store Composition

Stores can use other stores, enabling modular and composable state management.

// stores/auth.js
export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(null)
  
  const isAuthenticated = computed(() => !!user.value)
  
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    })
    const data = await response.json()
    user.value = data.user
    token.value = data.token
  }
  
  function logout() {
    user.value = null
    token.value = null
  }
  
  return { user, token, isAuthenticated, login, logout }
})

// stores/cart.js - uses auth store
export const useCartStore = defineStore('cart', () => {
  const authStore = useAuthStore() // ✅ Use another store
  
  const items = ref([])
  
  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  
  async function addItem(product) {
    if (!authStore.isAuthenticated) {
      throw new Error('Please login to add items')
    }
    
    items.value.push({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1,
    })
    
    // Sync with backend
    await fetch('/api/cart', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${authStore.token}`,
      },
      body: JSON.stringify({ productId: product.id }),
    })
  }
  
  return { items, total, addItem }
})

Pinia Plugins

// Persistence plugin
import { createPinia } from 'pinia'

function persistencePlugin({ store }) {
  // Load persisted state
  const stored = localStorage.getItem(store.$id)
  if (stored) {
    store.$patch(JSON.parse(stored))
  }
  
  // Persist on change
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

const pinia = createPinia()
pinia.use(persistencePlugin)
Vue

Performance & Best Practices

Vue Performance & Best Practices Optimize your Vue applications for maximum performance and follow best practices for maintainable, scalable code: Component Per

Vue Performance & Best Practices

Optimize your Vue applications for maximum performance and follow best practices for maintainable, scalable code:

Component Performance Optimization

v-once & v-memo

Use v-once to render elements only once and v-memo to memoize template sub-trees.

<template>
  <!-- v-once: renders only once, never updates -->
  <div v-once>
    <h1>{{ staticTitle }}</h1>
    <p>{{ staticDescription }}</p>
  </div>
  
  <!-- v-memo: memoizes based on dependency array -->
  <div v-memo="[count]">
    <!-- Only re-renders when count changes -->
    <p>Count: {{ count }}</p>
    <p>Other data: {{ otherData }}</p>
  </div>
  
  <!-- Memo for list items -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
    <!-- Only re-renders when id or selected changes -->
    {{ item.name }}
  </div>
</template>

Computed vs Methods

<script setup>
import { ref, computed } from 'vue'

const items = ref([/* lots of items */])

// ✅ Good: Computed - cached, only recalculates when items change
const expensiveComputation = computed(() => {
  console.log('Computing...')
  return items.value
    .filter(item => item.active)
    .map(item => item.price)
    .reduce((sum, price) => sum + price, 0)
})

// ❌ Bad: Method - recalculates on every render
const expensiveMethod = () => {
  console.log('Computing...')
  return items.value
    .filter(item => item.active)
    .map(item => item.price)
    .reduce((sum, price) => sum + price, 0)
}
</script>

<template>
  <div>
    <!-- Computed: Cached, efficient -->
    <p>Total: {{ expensiveComputation }}</p>
    
    <!-- Method: Recalculates every time -->
    <p>Total: {{ expensiveMethod() }}</p>
  </div>
</template>

Virtual Scrolling

For rendering large lists, use virtual scrolling to only render visible items.

// Using vue-virtual-scroller
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const items = ref(
  Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i}`,
  }))
)
</script>

<template>
  <RecycleScroller
    :items="items"
    :item-size="80"
    key-field="id"
    class="scroller"
    style="height: 600px"
  >
    <template #default="{ item }">
      <div class="item">
        <h3>{{ item.name }}</h3>
        <p>{{ item.description }}</p>
      </div>
    </template>
  </RecycleScroller>
</template>

Lazy Loading Components

<script setup>
import { defineAsyncComponent } from 'vue'

// Lazy load heavy components
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// With loading and error states
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200, // Delay before showing loading component
  timeout: 3000, // Timeout for loading
})

const showChart = ref(false)
</script>

<template>
  <div>
    <button @click="showChart = true">Show Chart</button>
    
    <Suspense v-if="showChart">
      <template #default>
        <HeavyChart :data="chartData" />
      </template>
      <template #fallback>
        <div>Loading chart...</div>
      </template>
    </Suspense>
  </div>
</template>

Avoid Unnecessary Reactivity

<script setup>
import { ref, shallowRef, markRaw } from 'vue'

// For large, immutable data, use shallowRef
const hugeDataset = shallowRef({
  /* thousands of items */
})

// For non-reactive objects (like third-party class instances)
const chartInstance = shallowRef(null)

onMounted(() => {
  // markRaw prevents Vue from making it reactive
  chartInstance.value = markRaw(new Chart())
})

// Constants don't need to be reactive
const API_URL = 'https://api.example.com' // ✅ Not reactive
const config = { timeout: 5000 } // ✅ Not reactive

// Only make data reactive if it needs to trigger updates
const userSettings = ref({ theme: 'dark' }) // ✅ Needs reactivity
</script>

Production Build Optimization

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    // Enable tree-shaking
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // Remove console.log in production
        drop_debugger: true,
      },
    },
    // Code splitting
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-components': ['./src/components/ui'],
        },
      },
    },
    // Chunk size warnings
    chunkSizeWarningLimit: 1000,
  },
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia'],
  },
})

Best Practices

  • Use <script setup> for cleaner, more performant code

  • Prefer computed() over methods for derived data

  • Use shallowRef/shallowReactive for large data structures

  • Lazy load routes and heavy components

  • Use v-show for frequently toggled elements, v-if for rare toggles

  • Extract reusable logic into composables

  • Keep components focused and single-responsibility

Vue

Testing Vue Applications

Testing Vue Applications Comprehensive testing strategies for Vue applications using Vitest, Vue Test Utils, and end-to-end testing tools: Vitest - Modern Testi

Testing Vue Applications

Comprehensive testing strategies for Vue applications using Vitest, Vue Test Utils, and end-to-end testing tools:

Vitest - Modern Testing Framework

Vitest is a blazing fast unit test framework powered by Vite. It provides Jest-compatible APIs with better performance.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
})

// Basic test
import { describe, it, expect } from 'vitest'
import { sum } from './utils'

describe('sum', () => {
  it('adds two numbers', () => {
    expect(sum(1, 2)).toBe(3)
  })
})

Vue Test Utils - Component Testing

Vue Test Utils is the official testing library for Vue components. It provides utilities to mount components and interact with them.

import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

describe('Counter.vue', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5,
      },
    })
    
    expect(wrapper.text()).toContain('Count: 5')
  })
  
  it('increments when button clicked', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('Count: 1')
    
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('Count: 2')
  })
  
  it('emits update event', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted()).toHaveProperty('update')
    expect(wrapper.emitted('update')).toHaveLength(1)
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})

Testing with Pinia Stores

import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, it, expect } from 'vitest'
import TodoList from './TodoList.vue'
import { useTodoStore } from '@/stores/todos'

describe('TodoList with Pinia', () => {
  beforeEach(() => {
    // Create a fresh pinia instance for each test
    setActivePinia(createPinia())
  })
  
  it('displays todos from store', () => {
    const wrapper = mount(TodoList, {
      global: {
        plugins: [createPinia()],
      },
    })
    
    const store = useTodoStore()
    store.todos = [
      { id: 1, text: 'Test todo', completed: false },
    ]
    
    expect(wrapper.text()).toContain('Test todo')
  })
  
  it('adds todo when form submitted', async () => {
    const wrapper = mount(TodoList, {
      global: {
        plugins: [createPinia()],
      },
    })
    
    await wrapper.find('input').setValue('New todo')
    await wrapper.find('form').trigger('submit')
    
    const store = useTodoStore()
    expect(store.todos).toHaveLength(1)
    expect(store.todos[0].text).toBe('New todo')
  })
})

Testing Composables

import { describe, it, expect } from 'vitest'
import { useMouse } from './composables/useMouse'
import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'

describe('useMouse', () => {
  it('tracks mouse position', async () => {
    // Create a test component that uses the composable
    const TestComponent = defineComponent({
      setup() {
        const { x, y } = useMouse()
        return { x, y }
      },
      template: '<div>{{ x }}, {{ y }}</div>',
    })
    
    const wrapper = mount(TestComponent, {
      attachTo: document.body,
    })
    
    // Simulate mouse move
    const event = new MouseEvent('mousemove', {
      clientX: 100,
      clientY: 200,
    })
    window.dispatchEvent(event)
    
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.x).toBe(100)
    expect(wrapper.vm.y).toBe(200)
    
    wrapper.unmount()
  })
})

End-to-End Testing with Cypress

// cypress/e2e/todo-app.cy.js
describe('Vue Todo App', () => {
  beforeEach(() => {
    cy.visit('http://localhost:5173')
  })
  
  it('adds and completes todos', () => {
    // Add todo
    cy.get('[data-test="todo-input"]').type('Buy groceries')
    cy.get('[data-test="add-btn"]').click()
    
    // Verify added
    cy.contains('Buy groceries').should('be.visible')
    
    // Complete todo
    cy.get('[data-test="todo-checkbox"]').first().check()
    
    // Verify completed
    cy.get('[data-test="todo-item"]')
      .first()
      .should('have.class', 'completed')
  })
  
  it('filters todos', () => {
    // Add multiple todos
    const todos = ['Todo 1', 'Todo 2', 'Todo 3']
    todos.forEach(todo => {
      cy.get('[data-test="todo-input"]').type(todo)
      cy.get('[data-test="add-btn"]').click()
    })
    
    // Complete first
    cy.get('[data-test="todo-checkbox"]').first().check()
    
    // Filter completed
    cy.get('[data-test="filter-completed"]').click()
    cy.get('[data-test="todo-item"]').should('have.length', 1)
    
    // Filter active
    cy.get('[data-test="filter-active"]').click()
    cy.get('[data-test="todo-item"]').should('have.length', 2)
  })
})

Testing Best Practices

  • Test behavior, not implementation details

  • Use data-test attributes for stable selectors

  • Mock external dependencies (APIs, stores)

  • Test user interactions with await and $nextTick

  • Keep tests isolated and independent

Vue

Directives & Template Syntax

Vue Directives & Template Syntax Vue's template syntax and built-in directives provide powerful ways to declaratively bind data to the DOM: Text Interpolation &

Vue Directives & Template Syntax

Vue's template syntax and built-in directives provide powerful ways to declaratively bind data to the DOM:

Text Interpolation & v-bind

<template>
  <!-- Text interpolation -->
  <p>{{ message }}</p>
  <p>{{ count * 2 }}</p>
  <p>{{ ok ? 'YES' : 'NO' }}</p>
  
  <!-- v-bind for attributes -->
  <img v-bind:src="imageSrc" v-bind:alt="imageAlt" />
  
  <!-- Shorthand -->
  <img :src="imageSrc" :alt="imageAlt" />
  
  <!-- Dynamic attributes -->
  <button :[attributeName]="value">Button</button>
  
  <!-- Binding multiple attributes -->
  <div v-bind="objectOfAttrs"></div>
  
  <!-- Class binding -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[activeClass, errorClass]"></div>
  
  <!-- Style binding -->
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

Conditional Rendering

<template>
  <!-- v-if: Conditional rendering (removes from DOM) -->
  <div v-if="type === 'A'">
    Type A
  </div>
  <div v-else-if="type === 'B'">
    Type B
  </div>
  <div v-else>
    Type C
  </div>
  
  <!-- v-show: Toggle visibility (CSS display) -->
  <div v-show="isVisible">
    Toggleable content
  </div>
  
  <!-- Template for grouping without extra elements -->
  <template v-if="showSection">
    <h1>Title</h1>
    <p>Content</p>
  </template>
  
  <!-- Use v-if for infrequent toggles, v-show for frequent -->
  <Modal v-if="showModal" /> <!-- Heavy component, unmount when hidden -->
  <Tooltip v-show="showTooltip" /> <!-- Light component, keep in DOM -->
</template>

List Rendering

<template>
  <!-- Basic v-for -->
  <li v-for="item in items" :key="item.id">
    {{ item.text }}
  </li>
  
  <!-- v-for with index -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }}. {{ item.text }}
  </li>
  
  <!-- v-for with object -->
  <div v-for="(value, key, index) in user" :key="key">
    {{ index }}. {{ key }}: {{ value }}
  </div>
  
  <!-- v-for with range -->
  <span v-for="n in 10" :key="n">{{ n }}</span>
  
  <!-- v-for with v-if (avoid this pattern) -->
  <!-- ❌ Bad: v-if with v-for on same element -->
  <li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
    {{ todo.text }}
  </li>
  
  <!-- ✅ Good: Use computed to filter first -->
  <li v-for="todo in activeTodos" :key="todo.id">
    {{ todo.text }}
  </li>
  
  <!-- ✅ Alternative: Use template -->
  <template v-for="todo in todos" :key="todo.id">
    <li v-if="!todo.done">
      {{ todo.text }}
    </li>
  </template>
</template>

Event Handling

<template>
  <!-- Basic event handler -->
  <button @click="handleClick">Click Me</button>
  
  <!-- Inline handler -->
  <button @click="count++">{{ count }}</button>
  
  <!-- Method with parameters -->
  <button @click="greet('Hello')">Greet</button>
  
  <!-- Access event object -->
  <button @click="handleClick($event)">Click</button>
  
  <!-- Event modifiers -->
  <button @click.stop="doThis">Stop Propagation</button>
  <button @click.prevent="doThis">Prevent Default</button>
  <form @submit.prevent="onSubmit">Submit</form>
  <button @click.once="doThis">Only Once</button>
  <div @click.self="doThis">Only if target is self</div>
  
  <!-- Key modifiers -->
  <input @keyup.enter="submit" />
  <input @keyup.tab="handleTab" />
  <input @keyup.delete="handleDelete" />
  <input @keyup.esc="handleEscape" />
  <input @keyup.ctrl.enter="ctrlEnterHandler" />
  
  <!-- Mouse modifiers -->
  <button @click.left="handleLeftClick">Left Click</button>
  <button @click.right="handleRightClick">Right Click</button>
  <button @click.middle="handleMiddleClick">Middle Click</button>
  
  <!-- Modifier chaining -->
  <button @click.stop.prevent="doThis">Chained Modifiers</button>
</template>

Form Input Bindings

<script setup>
import { ref } from 'vue'

const text = ref('')
const checked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const multiSelect = ref([])
</script>

<template>
  <!-- Text input -->
  <input v-model="text" placeholder="Enter text" />
  <p>Text: {{ text }}</p>
  
  <!-- Checkbox -->
  <input type="checkbox" v-model="checked" />
  <p>Checked: {{ checked }}</p>
  
  <!-- Multiple checkboxes -->
  <input type="checkbox" value="Vue" v-model="checkedNames" />
  <input type="checkbox" value="React" v-model="checkedNames" />
  <input type="checkbox" value="Angular" v-model="checkedNames" />
  <p>Checked: {{ checkedNames }}</p>
  
  <!-- Radio -->
  <input type="radio" value="One" v-model="picked" />
  <input type="radio" value="Two" v-model="picked" />
  <p>Picked: {{ picked }}</p>
  
  <!-- Select -->
  <select v-model="selected">
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  
  <!-- Modifiers -->
  <input v-model.lazy="text" /> <!-- Update on change, not input -->
  <input v-model.number="age" type="number" /> <!-- Convert to number -->
  <input v-model.trim="message" /> <!-- Trim whitespace -->
</template>

Custom Directives

Custom directives allow you to directly manipulate the DOM when needed.

// directives/focus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  },
}

// directives/click-outside.js
export const vClickOutside = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  },
}

// Usage in component
<script setup>
import { vFocus } from './directives/focus'
import { vClickOutside } from './directives/click-outside'

const isOpen = ref(false)

const handleClickOutside = () => {
  isOpen.value = false
}
</script>

<template>
  <input v-focus placeholder="Auto-focused" />
  
  <div v-click-outside="handleClickOutside">
    <button @click="isOpen = true">Open Menu</button>
    <div v-if="isOpen" class="menu">
      Menu content
    </div>
  </div>
</template>
Vue

Interview Questions

Vue Interview Questions Comprehensive Vue.js interview questions covering fundamentals, reactivity, Composition API, and advanced topics: Fundamentals 1. What i

Vue Interview Questions

Comprehensive Vue.js interview questions covering fundamentals, reactivity, Composition API, and advanced topics:

Fundamentals

1. What is Vue.js and why use it?

Vue.js is a progressive JavaScript framework for building user interfaces. It's designed to be incrementally adoptable, with a core library focused on the view layer. It's approachable, versatile, and performant with a gentle learning curve.

2. What is the difference between ref() and reactive()?

  • ref(): Works with primitives and objects, requires .value in JS

  • reactive(): Only for objects, direct property access

  • ref() can be reassigned, reactive() cannot

3. What is the Composition API?

The Composition API is a set of APIs that allows you to author Vue components using imported functions instead of declaring options. It enables better code organization, reusability, and TypeScript support.

4. What is the difference between v-if and v-show?

  • v-if: True conditional rendering, elements added/removed from DOM

  • v-show: CSS-based toggle, element always in DOM

  • Use v-if for infrequent toggles, v-show for frequent

5. What are Vue lifecycle hooks?

Lifecycle hooks are functions called at specific stages: onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted. They allow you to run code at different points in a component's lifecycle.

Reactivity & Composition API

6. How does Vue's reactivity work?

Vue 3 uses ES6 Proxies to track property access and modifications. When you access a reactive property, Vue tracks it as a dependency. When you modify it, Vue triggers updates to all dependent components and computed properties.

7. When to use watch() vs watchEffect()?

  • watch(): Explicit about dependencies, access old and new values

  • watchEffect(): Automatic dependency tracking, runs immediately

  • Use watch() when you need the previous value or lazy execution

8. What are composables?

Composables are functions that leverage the Composition API to encapsulate and reuse stateful logic. They follow the naming convention of starting with "use" and can call other composables.

Components & Communication

9. How do you pass data from parent to child?

Use props. Define them with defineProps() in the child component and pass them as attributes in the parent template.

10. How do you pass data from child to parent?

Use custom events with defineEmits(). The child emits events with data, and the parent listens with @ (v-on).

11. What is provide/inject?

provide/inject allows ancestor components to provide data to all descendants without prop drilling. The ancestor uses provide() and descendants use inject().

12. What are slots and when to use them?

Slots allow parents to pass template content to children. Use named slots for multiple content areas and scoped slots to pass data from child to parent template.

Routing & State

13. What is Vue Router?

Vue Router is the official routing library for Vue.js. It enables navigation between different views, handles browser history, and supports nested routes, lazy loading, and navigation guards.

14. What is Pinia?

Pinia is the official state management library for Vue. It provides a simpler API than Vuex, better TypeScript support, and works seamlessly with the Composition API.

15. Pinia vs Vuex - what are the differences?

  • Pinia: Simpler API, no mutations, better TypeScript

  • Vuex: Legacy, requires mutations, more boilerplate

  • Pinia is recommended for new projects

Performance

16. How do you optimize Vue applications?

  • Use v-once for static content

  • Use v-memo to memoize template sub-trees

  • Lazy load routes and components

  • Use virtual scrolling for large lists

  • Prefer computed over methods for derived data

17. What is the Virtual DOM in Vue?

Vue uses a Virtual DOM to optimize rendering. It creates a JavaScript representation of the DOM, diffs changes, and applies minimal updates to the real DOM for better performance.

Advanced Topics

18. What are navigation guards?

Navigation guards are hooks that allow you to control navigation. Types include: global guards (beforeEach, afterEach), per-route guards (beforeEnter), and in-component guards (onBeforeRouteLeave, onBeforeRouteUpdate).

19. What are custom directives?

Custom directives allow you to directly manipulate the DOM. They have lifecycle hooks like mounted, updated, and unmounted. Use them for low-level DOM operations like focus management or third-party library integration.

20. What is Teleport?

Teleport allows you to render component template in a different part of the DOM tree, useful for modals, tooltips, and notifications that need to break out of their parent container.

21. What is Suspense?

Suspense allows you to handle async dependencies in component trees. It shows fallback content while waiting for async components to load, providing a better loading experience.

22. What is the difference between Options API and Composition API?

  • Options API: data(), methods, computed, traditional approach

  • Composition API: setup(), ref(), reactive(), better for code organization

  • Composition API is recommended for new projects

Practical Scenarios

23. How do you handle forms in Vue?

Use v-model for two-way binding, reactive() or ref() for form state, and computed() for validation. Libraries like VeeValidate can help with complex forms.

24. How would you implement authentication?

Create an auth store with Pinia, use navigation guards to protect routes, store tokens in localStorage/cookies, and provide user data via provide/inject or the store.

25. How do you handle API calls in Vue?

Use composables with watchEffect() or onMounted(), libraries like VueQuery (TanStack Query for Vue), or create dedicated API services. Handle loading and error states with reactive refs.

26. What is $nextTick and when to use it?

$nextTick() waits for the next DOM update cycle. Use it when you need to access updated DOM after data changes, especially when measuring elements or setting focus.

Common Pitfalls

27. Why does destructuring props lose reactivity?

Destructuring extracts primitive values, breaking the reactive connection. Use toRefs() to maintain reactivity when destructuring: const { count } = toRefs(props).

28. What are common Vue anti-patterns?

  • Mutating props directly

  • Using v-if and v-for on the same element

  • Not providing keys in v-for

  • Overusing watchers instead of computed

  • Making everything reactive when not needed

29. How do you optimize a large list?

Use virtual scrolling with vue-virtual-scroller, v-memo on list items, computed() to filter/sort data, and consider pagination or infinite scroll.

30. What is the difference between computed and methods?

Computed properties are cached and only re-evaluate when dependencies change. Methods run every time they're called. Use computed for derived data, methods for actions.

Keep your Vue knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever