LearnThatStack Ace your next interview

Vue.js.
Interview cheat sheet.

Quick reference for Vue.js - sectioned for fast scanning. Skim the part you're shaky on, walk in confident.

Frontend Development 17-section reference ~11 min read

Summary

Vue.js is a progressive JavaScript framework for building user interfaces that focuses on the view layer and can be incrementally adopted. Created by Evan You, Vue combines the best features of React (virtual DOM, component-based architecture) and Angular (templates, directives) while maintaining simplicity and flexibility. Vue 3, the latest major version, introduces the Composition API for better logic reuse and TypeScript support, while maintaining backward compatibility with the Options API. Its reactive data binding system, single-file components, and gentle learning curve make it an excellent choice for projects ranging from simple widgets to complex SPAs. With a rich ecosystem including Vue Router, Pinia (state management), and Vue DevTools, Vue provides everything needed for modern web development. This cheatsheet covers Vue fundamentals, Composition API, reactivity system, and best practices for technical interviews.

Core Concepts

Vue Instance

// Vue 3 - Composition API
import { createApp } from 'vue'
const app = createApp({
  data() {
    return { message: 'Hello Vue!' }
  }
})
app.mount('#app')

// Vue 2 - Options API
new Vue({
  el: '#app',
  data: { message: 'Hello Vue!' }
})

Template Syntax

<!-- Interpolation -->
<span>{{ message }}</span>

<!-- v-html (renders raw HTML) -->
<div v-html="rawHtml"></div>

<!-- Expressions -->
<div>{{ number + 1 }}</div>
<div>{{ ok ? 'YES' : 'NO' }}</div>

Directives

Essential Directives

<!-- v-if / v-else-if / v-else -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Not A/B</div>

<!-- v-show (toggles display) -->
<h1 v-show="isVisible">Hello!</h1>

<!-- v-for -->
<li v-for="(item, index) in items" :key="item.id">
  {{ index }}: {{ item.text }}
</li>

<!-- v-model (two-way binding) -->
<input v-model="message" />
<input v-model.trim="msg" />
<input v-model.number="age" type="number" />
<input v-model.lazy="msg" />

<!-- v-on (@ shorthand) -->
<button @click="doThis"></button>
<button @click.stop="doThis"></button>
<button @click.prevent="doThis"></button>
<button @click.once="doThis"></button>

<!-- v-bind (: shorthand) -->
<img :src="imageSrc" />
<div :class="{ active: isActive }"></div>
<div :style="{ color: activeColor }"></div>

v-if vs v-show

  • v-if: Conditional rendering (creates/destroys elements)
  • v-show: Always rendered, toggles CSS display
  • Use v-show for frequent toggles

Components

Component Registration

// Global Registration
app.component('my-component', {
  template: '<div>A custom component!</div>'
})

// Local Registration
export default {
  components: {
    'my-component': {
      template: '<div>A custom component!</div>'
    }
  }
}

// Single File Component (SFC)
// MyComponent.vue
<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  data() {
    return { message: 'Hello!' }
  }
}
</script>

Props

// Child Component
export default {
  props: {
    // Basic type check
    propA: Number,
    
    // Multiple possible types
    propB: [String, Number],
    
    // Required
    propC: {
      type: String,
      required: true
    },
    
    // Default value
    propD: {
      type: Number,
      default: 100
    },
    
    // Custom validator
    propE: {
      validator(value) {
        return ['success', 'warning'].includes(value)
      }
    }
  }
}

// Parent Component
<MyComponent :prop-a="42" prop-c="hello" />

Custom Events

// Child Component
export default {
  methods: {
    handleClick() {
      this.$emit('custom-event', 'payload')
    }
  }
}

// Parent Component
<MyComponent @custom-event="handleEvent" />

Slots

<!-- Child Component -->
<template>
  <div>
    <slot></slot> <!-- Default slot -->
    <slot name="header"></slot> <!-- Named slot -->
    <slot name="item" :item="item"></slot> <!-- Scoped slot -->
  </div>
</template>

<!-- Parent Component -->
<MyComponent>
  <template #default>Default content</template>
  <template #header>Header content</template>
  <template #item="{ item }">{{ item.name }}</template>
</MyComponent>

Lifecycle Hooks

Options API

export default {
  beforeCreate() { /* Before instance initialization */ },
  created() { /* After instance created, before mounting */ },
  beforeMount() { /* Before mounting to DOM */ },
  mounted() { /* After mounting to DOM */ },
  beforeUpdate() { /* Before re-render */ },
  updated() { /* After re-render */ },
  beforeUnmount() { /* Before component destruction */ },
  unmounted() { /* After component destroyed */ }
}

Composition API

import { 
  onBeforeMount, onMounted, 
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted 
} from 'vue'

export default {
  setup() {
    onMounted(() => console.log('Mounted!'))
    onUnmounted(() => console.log('Unmounted!'))
  }
}

Computed Properties & Watchers

Computed Properties

// Options API
export default {
  data() {
    return { firstName: 'John', lastName: 'Doe' }
  },
  computed: {
    // Getter only
    fullName() {
      return `${this.firstName} ${this.lastName}`
    },
    // Getter + Setter
    fullNameWithSetter: {
      get() {
        return `${this.firstName} ${this.lastName}`
      },
      set(newValue) {
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

// Composition API
import { computed, ref } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

Watchers

// Options API
export default {
  data() {
    return { question: '', answer: '' }
  },
  watch: {
    // Simple watcher
    question(newVal, oldVal) {
      this.getAnswer()
    },
    // Deep watcher
    someObject: {
      handler(newVal, oldVal) {
        console.log('Object changed')
      },
      deep: true,
      immediate: true
    }
  }
}

// Composition API
import { watch, watchEffect, ref } from 'vue'

const question = ref('')
const answer = ref('')

// Watch single ref
watch(question, (newVal, oldVal) => {
  getAnswer()
})

// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  // ...
})

// watchEffect (runs immediately and tracks dependencies)
watchEffect(() => {
  console.log(`Count is: ${count.value}`)
})

Composition API

Basic Setup

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

// Reactive state
const count = ref(0)
const state = reactive({ message: 'Hello' })

// Methods
const increment = () => count.value++

// Computed
const doubled = computed(() => count.value * 2)

// Lifecycle
onMounted(() => console.log('Component mounted'))

// Watch
watch(count, (newVal) => console.log(`Count: ${newVal}`))
</script>

Composables (Custom Hooks)

// useCounter.js
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  const decrement = () => count.value--
  
  return { count, increment, decrement }
}

// Component
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>

Vue Router

Basic Setup

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/user/:id', component: User },
  { 
    path: '/admin', 
    component: Admin,
    beforeEnter: (to, from) => {
      // Route guard
      return checkAuth()
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})
// Programmatic navigation
router.push('/users')
router.push({ name: 'user', params: { id: 123 }})
router.replace('/users')
router.go(-1)

// In template
<router-link to="/about">About</router-link>
<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>

Route Guards

// Global guards
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated) {
    return '/login'
  }
})

// Component guards
export default {
  beforeRouteEnter(to, from, next) {
    // Called before component is created
    next()
  },
  beforeRouteUpdate(to, from) {
    // Called when route changes but component is reused
  },
  beforeRouteLeave(to, from) {
    // Called when leaving the component
  }
}

State Management (Pinia)

Store Definition

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

// Composition API style
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  const increment = () => count.value++
  
  return { count, doubleCount, increment }
})

Using Store

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
counter.increment()
</script>

<template>
  <div>{{ counter.count }}</div>
  <div>{{ counter.doubleCount }}</div>
</template>

Reactivity System

Vue 3 Reactivity

import { ref, reactive, toRef, toRefs, readonly } from 'vue'

// ref - for primitives
const count = ref(0)
console.log(count.value) // Access with .value

// reactive - for objects
const state = reactive({ count: 0 })
console.log(state.count) // Direct access

// toRef - create ref from reactive property
const countRef = toRef(state, 'count')

// toRefs - convert reactive object to refs
const { count, name } = toRefs(state)

// readonly - create readonly reactive object
const readonlyState = readonly(state)

Reactivity Caveats

// Vue 2 - Array mutations
vm.$set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)

// Vue 2 - Object property addition
vm.$set(vm.userProfile, 'age', 27)

// Vue 3 - All operations are reactive by default
state.newProp = 'value' // This works!

Advanced Patterns

Provide/Inject

// Parent component
import { provide } from 'vue'
provide('user', { name: 'John' })

// Child component (any level deep)
import { inject } from 'vue'
const user = inject('user')

Dynamic Components

<component :is="currentComponent" />

<script>
import CompA from './CompA.vue'
import CompB from './CompB.vue'

export default {
  components: { CompA, CompB },
  data() {
    return { currentComponent: 'CompA' }
  }
}
</script>

Async Components

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

// With loading/error states
const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

Custom Directives

// Global directive
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

// Local directive
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus()
      }
    }
  }
}

// Usage
<input v-focus />

Performance Optimization

Keep-Alive

<keep-alive>
  <component :is="currentComponent" />
</keep-alive>

<!-- With props -->
<keep-alive :include="['CompA', 'CompB']" :max="10">
  <router-view />
</keep-alive>

Lazy Loading Routes

const routes = [
  {
    path: '/about',
    component: () => import('./views/About.vue')
  }
]

v-memo (Vue 3.2+)

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

Best Practices

Component Design

  • Keep components small and focused
  • Use props for parent→child communication
  • Use events for child→parent communication
  • Avoid mutating props directly
  • Use computed properties for derived state

Naming Conventions

  • Components: PascalCase (MyComponent)
  • Props: camelCase in JS, kebab-case in templates
  • Events: kebab-case (my-event)
  • Slots: kebab-case

Performance Tips

  • Use v-show for frequent toggles
  • Add :key to v-for items
  • Lazy load routes and components
  • Use computed properties instead of methods in templates
  • Debounce expensive operations

Testing Vue Applications

Unit Testing with Vitest

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

describe('Counter', () => {
  it('increments count when button is clicked', async () => {
    const wrapper = mount(Counter)
    
    expect(wrapper.text()).toContain('Count: 0')
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.text()).toContain('Count: 1')
  })
  
  it('emits custom event', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted()).toHaveProperty('increment')
    expect(wrapper.emitted().increment[0]).toEqual([1])
  })
})

Testing Composables

import { renderHook, act } from '@testing-library/vue-hooks'
import { useCounter } from './useCounter'

test('counter increments', () => {
  const { result } = renderHook(() => useCounter())
  
  expect(result.current.count.value).toBe(0)
  
  act(() => {
    result.current.increment()
  })
  
  expect(result.current.count.value).toBe(1)
})

Common Patterns & Solutions

Global State Without Vuex/Pinia

// Simple reactive store
import { reactive, readonly } from 'vue'

const state = reactive({
  user: null,
  isAuthenticated: false
})

const mutations = {
  setUser(user) {
    state.user = user
    state.isAuthenticated = !!user
  },
  logout() {
    state.user = null
    state.isAuthenticated = false
  }
}

export default {
  state: readonly(state),
  ...mutations
}

Form Validation Pattern

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

const email = ref('')
const password = ref('')

const errors = computed(() => {
  const errors = {}
  
  if (!email.value.includes('@')) {
    errors.email = 'Invalid email'
  }
  
  if (password.value.length < 8) {
    errors.password = 'Password must be at least 8 characters'
  }
  
  return errors
})

const isValid = computed(() => Object.keys(errors.value).length === 0)
</script>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'

const searchQuery = ref('')
const searchResults = ref([])

const debouncedSearch = debounce(async (query) => {
  const results = await api.search(query)
  searchResults.value = results
}, 300)

watch(searchQuery, (newQuery) => {
  if (newQuery) {
    debouncedSearch(newQuery)
  } else {
    searchResults.value = []
  }
})

Interview Preparation Checklist

  • ✅ Core Concepts: Reactivity, components, directives
  • ✅ Template Syntax: Interpolation, directives, event handling
  • ✅ Component Communication: Props, events, slots, provide/inject
  • ✅ Lifecycle Hooks: Options API vs Composition API
  • ✅ State Management: Local state, Pinia, composables
  • ✅ Routing: Vue Router, guards, lazy loading
  • ✅ Composition API: setup, ref, reactive, computed, watch
  • ✅ Performance: keep-alive, lazy loading, v-memo
  • ✅ Testing: Unit tests, component testing, E2E
  • ✅ Vue 3 Features: Teleport, Suspense, Fragments

Key Takeaway: Vue.js excels at providing a progressive, approachable framework that scales from simple widgets to complex applications. Its reactive data binding, component system, and excellent tooling make it highly productive. Master the reactivity system, understand when to use Composition vs Options API, and know the ecosystem tools (Vue Router, Pinia) for successful Vue development.

Component Design

  • Keep components small and focused
  • Use props for parent→child communication
  • Use events for child→parent communication
  • Avoid mutating props directly
  • Use computed properties for derived state

Naming Conventions

  • Components: PascalCase (MyComponent)
  • Props: camelCase in JS, kebab-case in templates
  • Events: kebab-case (my-event)
  • Slots: kebab-case

Performance Tips

  • Use v-show for frequent toggles
  • Add :key to v-for items
  • Lazy load routes and components
  • Use computed properties instead of methods in templates
  • Debounce expensive operations

Key Interview Concepts

Vue vs Other Frameworks

Feature Vue React Angular
Learning Curve Gentle Moderate Steep
Data Binding Two-way One-way Two-way
Architecture MVVM Component-based MVC/Component
Template Syntax HTML-based JSX HTML + TypeScript
State Management Pinia/Vuex Redux/Context Services/RxJS
Performance Fast Fast Good
TypeScript Optional Optional Required
Bundle Size ~34KB ~42KB ~130KB

Reactivity System Deep Dive

// Vue 2 - Object.defineProperty
Object.defineProperty(obj, 'count', {
  get() { /* track dependencies */ },
  set(newVal) { /* trigger updates */ }
})

// Vue 3 - Proxy
const state = new Proxy(target, {
  get(target, key, receiver) {
    track(target, key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    trigger(target, key)
    return Reflect.set(target, key, value, receiver)
  }
})

Composition API vs Options API

Aspect Options API Composition API
Organization By options (data, methods) By logical concern
Type Inference Limited Excellent
Code Reuse Mixins (problematic) Composables (clean)
Tree-shaking Limited Better
Learning Curve Easier Steeper initially
Bundle Size Slightly larger Optimizable

Computed vs Methods vs Watch

Feature Computed Methods Watch
Caching Yes No N/A
Dependencies Automatic tracking None Explicit
Use Case Derived state Event handlers Side effects
Return Value Required Optional None
Async No Yes Yes

Virtual DOM & Diffing Algorithm

// Simplified Vue Virtual DOM process
1. Template → Render Function → Virtual DOM Tree
2. State Change → New Virtual DOM Tree
3. Diff Algorithm (Compare Old vs New)
4. Patch Operations → Update Real DOM

// Key optimizations:
- Static hoisting (compile-time)
- Patch flags (targeted updates)
- Fragment support (multiple roots)
- Cached event handlers

Vue 2 vs Vue 3 Migration Guide

Feature Vue 2 Vue 3
Reactivity Object.defineProperty Proxy
API Options API Options + Composition
Root Elements Single Multiple (Fragments)
v-model Single Multiple on component
Lifecycle beforeDestroy/destroyed beforeUnmount/unmounted
Async Components () => import() defineAsyncComponent
Filters Supported Removed (use methods)
TypeScript Plugin-based First-class support
Found this useful? Pass it on.
Pro · $10/mo

The sheet is free. Pro goes deeper.

Pro opens the full question library behind every sheet, every refresher and a monthly AI allowance. One subscription, all formats.

Full question library All refreshers Cancel anytime