Toolmingo
Guides21 min read

50 Vue.js Interview Questions (With Answers)

Top Vue 3 interview questions with clear answers and code examples — covering Composition API, reactivity, Pinia, Vue Router, performance, and modern Vue patterns.

Vue.js interviews test component design, reactivity system understanding, state management, and Composition API fluency. This guide covers the 50 most common Vue 3 questions — with concise answers and runnable code examples.

Quick reference

Topic Most asked questions
Fundamentals Options vs Composition API, reactivity, SFC
Reactivity ref vs reactive, computed, watch
Components props, emits, slots, provide/inject
Vue Router navigation guards, dynamic routes, lazy loading
State management Pinia, Vuex vs Pinia
Performance v-memo, KeepAlive, async components

Fundamentals

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

Feature Options API Composition API
Organisation By option type (data/methods/computed) By feature/concern
Code reuse Mixins (namespace collisions) Composables (explicit)
TypeScript Awkward (this typing) Excellent (plain functions)
Introduced Vue 2 Vue 3 (also backported to Vue 2.7)
Verbosity More boilerplate Less, but more flexible
<!-- 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>

Both APIs are fully supported in Vue 3. Composition API is recommended for new projects.


2. What is a Single File Component (SFC)?

An SFC is a .vue file that encapsulates template, script, and style in one file.

<template>
  <button @click="count++">{{ count }}</button>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<style scoped>
button { padding: 8px 16px; }
</style>

<style scoped> adds a unique data attribute so styles only apply to the current component.


3. What is Vue's reactivity system?

Vue 3 uses Proxy-based reactivity (replacing Vue 2's Object.defineProperty). When you access or mutate reactive data, Vue tracks dependencies via a dependency tracking system (effect tracking).

import { reactive } from 'vue'
const state = reactive({ count: 0 })
// Accessing state.count inside a computed/watchEffect registers a dependency
// Mutating state.count triggers re-renders of all dependents

Benefits over Vue 2:

  • Detects property additions/deletions (no Vue.set needed)
  • Works with Arrays natively
  • Supports Map, Set, WeakMap, WeakSet

4. What is the difference between ref and reactive?

ref reactive
Wraps Any value (primitives + objects) Objects only
Access in JS .value required Direct property access
Access in template Auto-unwrapped (no .value) Direct
Destructuring Safe (keep reactivity) Loses reactivity (use toRefs)
Reassignment ref.value = newObj works Must mutate properties
import { ref, reactive, toRefs } from 'vue'

// ref — primitives need .value
const count = ref(0)
count.value++

// reactive — objects, no .value
const state = reactive({ count: 0, name: 'Vue' })
state.count++

// Destructuring reactive loses reactivity — use toRefs
const { count: countRef } = toRefs(state)

Rule of thumb: Use ref for primitives, either for objects. Many teams use ref exclusively for consistency.


5. What is computed and how does it differ from a method?

computed is cached based on its reactive dependencies. A method runs every time it's called.

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

const items = ref([1, 2, 3, 4, 5])

// Cached — only recomputes when `items` changes
const evenItems = computed(() => items.value.filter(n => n % 2 === 0))

// Runs every render call, even if items hasn't changed
function getEvenItems() {
  return items.value.filter(n => n % 2 === 0)
}
</script>

Use computed for derived state, methods for event handlers and imperative actions.


6. How does watch differ from watchEffect?

watch watchEffect
Source Explicit (ref, reactive, getter) Auto-tracked (runs immediately)
Old value Provides (newVal, oldVal) Not available
Lazy by default Yes (unless { immediate: true }) No — runs on mount
Use case React to specific source changes Side effects that depend on reactive state
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)

// watch — explicit, lazy
watch(count, (newVal, oldVal) => {
  console.log(`${oldVal} → ${newVal}`)
})

// watchEffect — auto-tracks, eager
watchEffect(() => {
  console.log('count is', count.value) // runs immediately
})

7. What is defineProps and defineEmits in <script setup>?

Compiler macros that declare a component's public interface without imports.

<script setup>
// Props with types and defaults
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})

// TypeScript alternative
// const props = defineProps<{ title: string; count?: number }>()
// const { count = 0 } = withDefaults(defineProps<...>(), { count: 0 })

// Emits declaration
const emit = defineEmits(['update:count', 'close'])

// Usage
emit('update:count', props.count + 1)
</script>

8. How does two-way binding work with v-model on components?

v-model on a component expands to a prop + emit pair.

<!-- Parent -->
<MyInput v-model="name" />
<!-- Same as: <MyInput :modelValue="name" @update:modelValue="name = $event" /> -->

<!-- MyInput.vue -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
  <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

Vue 3.4+ introduces defineModel() — a shorthand that handles this automatically:

<script setup>
const model = defineModel() // creates modelValue prop + update:modelValue emit
</script>
<template>
  <input v-model="model" />
</template>

9. What are slots? Explain named and scoped slots.

Slots let a parent inject content into a child's template.

<!-- Child: Card.vue -->
<template>
  <div class="card">
    <header><slot name="header" /></header>
    <main><slot /></main>  <!-- default slot -->
    <footer>
      <!-- Scoped slot: exposes data to parent -->
      <slot name="footer" :stats="{ views: 42 }" />
    </footer>
  </div>
</template>

<!-- Parent usage -->
<Card>
  <template #header>My Title</template>
  <p>Card body content</p>
  <template #footer="{ stats }">
    <span>Views: {{ stats.views }}</span>
  </template>
</Card>

10. What is provide / inject and when should you use it?

provide/inject passes data from an ancestor to any descendant — avoiding prop drilling.

<!-- Ancestor -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme) // provide reactive value
</script>

<!-- Deep descendant -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 'light' is default
</script>

Use for cross-cutting concerns (theme, locale, auth) shared across a subtree. For global app state, prefer Pinia.


Reactivity & Composition

11. What are composables and how do you write one?

A composable is a function that uses Vue's Composition API to encapsulate reusable stateful logic.

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

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url.value ?? url)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

// Component usage
const { data, error, loading } = useFetch('https://api.example.com/users')

Naming convention: use prefix (same as React hooks).


12. What is toRefs and why is it needed?

toRefs converts a reactive object's properties into individual refs — preserving reactivity after destructuring.

import { reactive, toRefs } from 'vue'

const state = reactive({ x: 0, y: 0 })

// WRONG — loses reactivity
const { x, y } = state

// CORRECT — each is a ref
const { x, y } = toRefs(state)
// x.value / y.value are reactive

Commonly used in composables that return reactive state.


13. How does shallowRef / shallowReactive differ from ref / reactive?

Deep version Shallow version
Reactivity depth All nested properties Root level only
Performance More overhead Better for large objects
Use case Most cases Large read-only datasets, external libs
import { shallowRef } from 'vue'
const state = shallowRef({ nested: { count: 0 } })

// This WON'T trigger reactivity
state.value.nested.count++

// This WILL trigger reactivity (replaces root)
state.value = { nested: { count: 1 } }

14. What is markRaw and when do you use it?

markRaw marks an object so Vue's reactivity system skips it. Useful for third-party library instances (e.g., Chart.js, Three.js) that must not be proxied.

import { reactive, markRaw } from 'vue'

const chart = markRaw(new Chart(canvas, config))
const state = reactive({
  chart, // Not converted to a Proxy — Chart.js methods work correctly
  label: 'Sales'
})

15. Explain the component lifecycle in Vue 3.

setup()         ← Composition API entry point (replaces beforeCreate + created)
  ↓
onBeforeMount   ← DOM not yet created
  ↓
onMounted       ← DOM is ready, refs accessible
  ↓
onBeforeUpdate  ← Reactive data changed, DOM not yet updated
  ↓
onUpdated       ← DOM re-rendered
  ↓
onBeforeUnmount ← Component about to be destroyed
  ↓
onUnmounted     ← Cleanup (timers, subscriptions, event listeners)
<script setup>
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
  window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
</script>

Templates & Directives

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

v-if v-show
Renders to DOM Only when true Always
Toggle cost High (create/destroy) Low (CSS display)
Initial render cost Low when false Higher
Use when Condition rarely changes Toggling frequently
<div v-if="isLoggedIn">Dashboard</div>   <!-- removed from DOM when false -->
<div v-show="isMenuOpen">Menu</div>       <!-- display:none when false -->

17. How does v-for work and why are keys important?

v-for renders a list from an array, object, or range.

<ul>
  <li v-for="(item, index) in items" :key="item.id">
    {{ index + 1 }}. {{ item.name }}
  </li>
</ul>

Keys give Vue a stable identity for each node. Without unique keys:

  • Vue may reuse wrong DOM nodes
  • Component state gets mixed up
  • Animations break

Never use the array index as a key when the list can be reordered or filtered.


18. What is v-bind shorthand and what can it bind to?

v-bind:attr can be shortened to :attr. It binds JS expressions to HTML attributes, DOM properties, component props, and event listeners.

<img :src="imageUrl" :alt="imageAlt" />
<button :disabled="isLoading">Submit</button>
<MyComponent :user="currentUser" />

<!-- Bind entire object of props at once -->
<MyComponent v-bind="userProps" />

19. What event modifiers does Vue provide?

Event modifiers are chained with . after the event name.

<!-- Stop propagation -->
<button @click.stop="handleClick">Click</button>

<!-- Prevent default -->
<form @submit.prevent="submitForm">...</form>

<!-- Only trigger once -->
<button @click.once="trackFirstClick">Track</button>

<!-- Only trigger from element itself, not children -->
<div @click.self="handleBackdropClick">
  <Modal />
</div>

<!-- Key modifiers -->
<input @keyup.enter="search" @keyup.esc="clearSearch" />

20. What is v-memo and when should you use it?

v-memo memoises a subtree of the template — it skips re-rendering when the dependency array hasn't changed.

<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
  <!-- Only re-renders when item.selected changes -->
  <HeavyComponent :item="item" />
</div>

Use for large lists where most items rarely change. Don't use prematurely — measure first.


Vue Router

21. How do you define routes with Vue Router 4?

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./views/Home.vue') }, // lazy load
    { path: '/users/:id', component: () => import('./views/User.vue') },
    { path: '/admin', component: AdminLayout,
      children: [
        { path: '', component: AdminDashboard },
        { path: 'settings', component: AdminSettings }
      ]
    },
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
  ]
})

22. How do you access route params and query strings?

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

const route = useRoute()
// /users/42?tab=profile
console.log(route.params.id)    // '42'
console.log(route.query.tab)    // 'profile'
console.log(route.name)         // named route
console.log(route.fullPath)     // '/users/42?tab=profile'
</script>

23. What are navigation guards and when do you use them?

Navigation guards intercept route changes to redirect, cancel, or add side effects.

// Global guard
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// Per-route guard
{
  path: '/admin',
  component: Admin,
  beforeEnter: (to, from) => {
    if (!isAdmin()) return '/403'
  }
}
<!-- In-component guard -->
<script setup>
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    return confirm('Leave without saving?')
  }
})
</script>

24. How do you programmatically navigate with Vue Router?

<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

function goToUser(id) {
  router.push({ name: 'User', params: { id } })
}

function goBack() {
  router.back()
}

function replaceRoute() {
  // Replaces current history entry (no back button)
  router.replace('/dashboard')
}
</script>

25. What is <RouterView> and <RouterLink>?

  • <RouterView> is a placeholder that renders the matched route's component.
  • <RouterLink> renders an <a> tag that navigates without page reload and automatically adds active-class when the route matches.
<!-- App.vue -->
<nav>
  <RouterLink to="/" exact-active-class="active">Home</RouterLink>
  <RouterLink :to="{ name: 'User', params: { id: 1 } }">Profile</RouterLink>
</nav>
<RouterView />

State Management (Pinia)

26. What is Pinia and how does it differ from Vuex?

Feature Pinia Vuex 4
API Composition API style Options-style (state/getters/mutations/actions)
Mutations Removed — directly mutate state Required for state changes
TypeScript Excellent (inferred types) Verbose, needs type casting
Devtools Full support Full support
Bundle size ~1KB Larger
Modules Flat stores (no namespacing needed) Nested modules with namespacing

Pinia is the official recommended state manager for Vue 3.


27. How do you create and use a Pinia store?

// stores/useCounterStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0)
  // Getters
  const double = computed(() => count.value * 2)
  // Actions
  function increment() { count.value++ }
  async function fetchAndSet(url) {
    const res = await fetch(url)
    count.value = (await res.json()).count
  }

  return { count, double, increment, fetchAndSet }
})
<!-- Component usage -->
<script setup>
import { useCounterStore } from '@/stores/useCounterStore'
import { storeToRefs } from 'pinia'

const store = useCounterStore()
// storeToRefs preserves reactivity when destructuring
const { count, double } = storeToRefs(store)
</script>
<template>
  <p>{{ count }} × 2 = {{ double }}</p>
  <button @click="store.increment()">+</button>
</template>

28. Why must you use storeToRefs when destructuring Pinia stores?

Destructuring a store directly breaks reactivity for state and getters (actions are fine to destructure).

const store = useCounterStore()

// WRONG — count / double are plain values, not reactive
const { count, double, increment } = store

// CORRECT — count and double are refs
const { count, double } = storeToRefs(store)
const { increment } = store // actions don't need storeToRefs

29. How do you persist Pinia state across page refreshes?

Use the pinia-plugin-persistedstate plugin.

// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// store
export const useUserStore = defineStore('user', () => {
  const token = ref(null)
  return { token }
}, {
  persist: true // saves to localStorage by default
})

30. How do you reset a Pinia store?

With the Setup Store API, implement a $reset manually (it's built-in only for Options Stores):

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('')

  function $reset() {
    count.value = 0
    name.value = ''
  }

  return { count, name, $reset }
})

Performance

31. What is <KeepAlive> and when do you use it?

<KeepAlive> caches component instances — preventing destroy/recreate cycles when toggling.

<KeepAlive :include="['TabA', 'TabB']" :max="5">
  <component :is="currentTab" />
</KeepAlive>

The cached component receives onActivated / onDeactivated lifecycle hooks instead of onMounted / onUnmounted.

Use for: tabs, wizards, heavy components where remounting is expensive.


32. How do you lazily load components in Vue?

// Async component (Vue 3)
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: Spinner,
  errorComponent: ErrorDisplay,
  delay: 200,   // show loading after 200ms
  timeout: 10000
})

In Vue Router, all route components should be lazy-loaded:

{ path: '/dashboard', component: () => import('./views/Dashboard.vue') }

33. What is Suspense in Vue 3?

<Suspense> renders a fallback while async setup components or async components are loading.

<Suspense>
  <template #default>
    <AsyncUserProfile />  <!-- component with async setup() -->
  </template>
  <template #fallback>
    <LoadingSpinner />
  </template>
</Suspense>
<!-- AsyncUserProfile.vue -->
<script setup>
// Async setup is allowed
const user = await fetchUser(userId)
</script>

34. How does Vue handle list rendering performance?

  1. Use stable, unique keys (item.id, not array index)
  2. v-memo to skip subtree re-rendering when dependencies unchanged
  3. Virtual scrolling (e.g., vue-virtual-scroller) for lists > 1000 items
  4. shallowRef for large datasets where deep reactivity is unnecessary
  5. Object.freeze for completely static data

35. What is Transition and TransitionGroup?

<Transition> animates a single element entering/leaving.
<TransitionGroup> animates list items (supports move animations via FLIP).

<Transition name="fade">
  <p v-if="show">Hello</p>
</Transition>

<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

Available CSS classes: {name}-enter-from, {name}-enter-active, {name}-enter-to, and leave equivalents.


Advanced Topics

36. How do you create a custom directive in Vue 3?

// Directive object
const vFocus = {
  mounted(el) { el.focus() }
}

// Globally registered
app.directive('focus', {
  mounted(el, binding) {
    if (binding.value !== false) el.focus()
  }
})

// Usage
<input v-focus />
<input v-focus="shouldFocus" />

Directive hooks: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted.


37. What is Teleport and when do you need it?

<Teleport> renders a component's template in a different part of the DOM — useful for modals and tooltips that need to escape CSS stacking contexts.

<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal-overlay">
    <div class="modal">
      <p>Modal content</p>
      <button @click="open = false">Close</button>
    </div>
  </div>
</Teleport>

The component stays in the Vue component tree (props/events work normally) but renders to body.


38. How does Vue handle TypeScript?

Vue 3 is written in TypeScript and has first-class TS support.

<script setup lang="ts">
import { ref } from 'vue'

interface User {
  id: number
  name: string
}

// Type-safe props
const props = defineProps<{
  title: string
  user?: User
}>()

// withDefaults for default values
const { title, user = { id: 0, name: 'Guest' } } = withDefaults(
  defineProps<{ title: string; user?: User }>(),
  { user: () => ({ id: 0, name: 'Guest' }) }
)

const count = ref<number>(0)
const emit = defineEmits<{
  update: [value: number]  // typed named tuple
  close: []
}>()
</script>

39. What is the difference between defineExpose and normal <script setup>?

In <script setup>, all bindings are private by default. defineExpose selectively exposes bindings to parent refs.

<!-- ChildInput.vue -->
<script setup>
import { ref } from 'vue'
const inputRef = ref(null)
const value = ref('')

defineExpose({ focus: () => inputRef.value?.focus(), value })
</script>

<!-- Parent -->
<script setup>
const childRef = ref(null)
function focusChild() {
  childRef.value?.focus() // calls exposed method
}
</script>
<template>
  <ChildInput ref="childRef" />
</template>

40. How do Vue 3 and Vue 2 differ in breaking ways?

Change Vue 2 Vue 3
Global API Vue.component(), Vue.use() app.component(), app.use()
v-model :value + @input :modelValue + @update:modelValue
Fragments Single root element required Multiple root elements allowed
Filters {{ date | format }} Removed (use methods/computed)
$children Available Removed
Reactivity Object.defineProperty Proxy
$listeners Separate from $attrs Merged into $attrs
Transition classes v-enter v-enter-from

Testing

41. How do you test Vue components with Vue Test Utils?

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

describe('Counter', () => {
  it('increments on click', async () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 5 }
    })
    expect(wrapper.text()).toContain('5')
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('6')
  })

  it('emits update event', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('update')).toBeTruthy()
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})

42. How do you test Pinia stores?

import { setActivePinia, createPinia } from 'pinia'
import { beforeEach, describe, it, expect } from 'vitest'
import { useCounterStore } from './useCounterStore'

describe('Counter Store', () => {
  beforeEach(() => setActivePinia(createPinia()))

  it('increments', () => {
    const store = useCounterStore()
    expect(store.count).toBe(0)
    store.increment()
    expect(store.count).toBe(1)
    expect(store.double).toBe(2)
  })
})

43. What is shallowMount vs mount?

mount shallowMount
Child components Rendered fully Stubbed out
Speed Slower Faster
Use case Integration tests, real interactions Unit testing component in isolation
// shallowMount stubs <ChildComponent> with <child-component-stub>
const wrapper = shallowMount(Parent)
expect(wrapper.findComponent({ name: 'ChildComponent' }).exists()).toBe(true)

Common Pitfalls

44. Why is mutating props directly a bad practice?

Vue enforces one-way data flow. Mutating a prop:

  • Creates unpredictable state (parent and child out of sync)
  • Generates a Vue warning
  • Breaks the component contract

Correct patterns:

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

// WRONG: props.modelValue = newVal
// CORRECT: emit
function handleInput(e) {
  emit('update:modelValue', e.target.value)
}

// Or use a writable computed
const localValue = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>

45. What causes "Maximum recursive updates exceeded" in Vue?

Triggered when a watch or computed causes a state change that re-triggers itself.

// BAD — infinite loop
watch(count, (val) => {
  count.value = val + 1 // triggers watch again
})

// BAD — computed with side effect
const bad = computed(() => {
  otherRef.value++ // side effect in computed!
  return count.value * 2
})

Fix: avoid side effects in computed, ensure watch handlers don't re-trigger their own source.


46. Why doesn't v-for work with v-if on the same element?

In Vue 3, v-if has higher priority than v-for when on the same element — so v-if can't access the v-for variable.

<!-- WRONG — `item` is not in scope for v-if -->
<li v-for="item in items" v-if="item.active">{{ item.name }}</li>

<!-- CORRECT — wrap with template -->
<template v-for="item in items" :key="item.id">
  <li v-if="item.active">{{ item.name }}</li>
</template>

<!-- BETTER — filter before rendering -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>

47. What happens if you lose reactivity in a reactive object?

// LOST — reassigning breaks reactivity
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // the old state object is no longer tracked

// LOST — destructuring
const { count } = state // plain number

// SOLUTION: use ref instead of reactive for root reassignment
const state = ref({ count: 0 })
state.value = { count: 1 } // works

// SOLUTION: for destructuring, use toRefs
const { count } = toRefs(state)

Common mistakes

Mistake Problem Fix
Using array index as :key Wrong elements reused on sort/filter Use stable unique IDs
Mutating props directly One-way flow broken, Vue warning Emit events to parent
Watching deep objects without { deep: true } Nested changes not detected Add { deep: true } or use watchEffect
Accessing .value in template Auto-unwrapped in templates Remove .value in template
Async setup without <Suspense> Top-level await silently fails Wrap async component in <Suspense>
Reactive state in v-for without :key Wrong DOM reuse Always add :key
Blocking computed with async Computed must be synchronous Use watchEffect + ref for async derived state
Not cleaning up in onUnmounted Memory leaks (timers, subscriptions) Use onUnmounted or useEventListener composable

Vue vs React vs Angular

Feature Vue 3 React 18 Angular 17
Learning curve Low Medium High
Reactivity Proxy (built-in) Manual (useState/useEffect) Signals / Zone.js
Template HTML-like templates JSX HTML templates
State management Pinia (official) External (Redux/Zustand) NgRx / Signals
TypeScript Excellent Excellent Excellent (built-in)
Bundle size ~34KB (runtime) ~6KB (react+dom) ~180KB+
Two-way binding v-model Manual [(ngModel)]
Official router Vue Router React Router (unofficial) Built-in

FAQ

Q: Should I use Options API or Composition API for a new Vue 3 project?
A: Use Composition API with <script setup>. It offers better TypeScript support, more flexible code organisation, and composables for reuse. Options API is still fully supported and valid for simpler components.

Q: What is the difference between ref and reactive — which should I always use?
A: ref works for any type, requires .value in JS but not in templates. reactive is for objects only, no .value needed but can't be reassigned. Many teams use ref exclusively for consistency. Either is fine.

Q: When should I use Pinia vs local component state?
A: Use local state (ref/reactive) for data that belongs only to a component or its subtree. Use Pinia when multiple unrelated components need the same data, when you need to persist state, or when debugging complex flows with devtools.

Q: Is $store gone in Vue 3?
A: Vuex 4 (with $store) still works in Vue 3 but Pinia is the recommended replacement. Pinia has no $store property — you import and call the store composable directly.

Q: How do I handle global error handling in Vue?
A: Use app.config.errorHandler:

app.config.errorHandler = (err, instance, info) => {
  // log to Sentry / your error tracker
  console.error(err, info)
}

Q: How do I migrate a Vue 2 project to Vue 3?
A: Use the official Vue Migration Build (@vue/compat) for a incremental migration. Main steps: upgrade Vue, migrate global API (Vue.componentapp.component), update v-model, replace filters with methods, handle composition API vs Options API per component. Use the vue-router v4 and Pinia (replacing Vuex) along the way.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools