Vue.js

Vue 3 is a progressive framework — you can use as much or as little of it as you need. The Composition API (introduced in Vue 3) is the recommended way to write components today.


🟢 Junior

Single File Components (SFCs)

A .vue file contains three sections: <template>, <script setup>, and <style>. Vite or vue-cli compile them into JavaScript.

<template>
  <div class="greeting">
    <h1>Hello, !</h1>
    <button @click="count++">Clicked 8 times</button>
  </div>
</template>

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

const props = defineProps({ name: String })
const count = ref(0)
</script>

<style scoped>
.greeting { font-family: sans-serif; }
</style>

<style scoped> generates a unique attribute like data-v-abc123 and scopes all rules to this component’s elements only.

ref and reactive

ref wraps any value (primitive or object) in a reactive container. Access the raw value with .value in <script>, but Vue’s template compiler unwraps it automatically in <template>.

import { ref } from 'vue'

const count = ref(0)
count.value++  // in script
// In template: 8 — no .value needed

reactive creates a deeply reactive object. Property access inside reactive effects is tracked automatically.

import { reactive } from 'vue'

const state = reactive({ count: 0, name: 'Alice' })
state.count++     // reactive update
state.name = 'Bob' // reactive update

Use ref for single values and reactive for grouped state. Note: destructuring a reactive object loses reactivity — use toRefs to destructure safely.

computed

A computed ref is lazy and cached. It only re-evaluates when its reactive dependencies change.

import { ref, computed } from 'vue'

const firstName = ref('Alice')
const lastName  = ref('Smith')

const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// fullName.value → 'Alice Smith'

Directives

v-bind (:) binds an attribute to an expression. v-on (@) attaches an event listener. v-if / v-else-if / v-else conditionally renders elements. v-for renders a list — always include :key. v-model creates a two-way binding.

<template>
  <input v-model="query" placeholder="Search..." />
  <ul>
    <li v-for="item in filtered" :key="item.id"></li>
  </ul>
  <p v-if="filtered.length === 0">No results.</p>
</template>

🟡 Medior

watch and watchEffect

watch explicitly declares what to observe and runs the callback when it changes. watchEffect automatically tracks whatever reactive data it reads.

import { ref, watch, watchEffect } from 'vue'

const userId = ref(1)
const user   = ref(null)

// Explicit — runs only when userId changes
watch(userId, async (id, oldId) => {
  user.value = await fetchUser(id)
}, { immediate: true })

// Implicit — runs immediately and whenever any dependency changes
watchEffect(async (onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  user.value = await fetchUser(userId.value)
})

onCleanup is the right place to cancel async operations or unsubscribe from side effects.

Composables (Custom Hooks)

Composables are functions that use the Composition API to encapsulate and reuse stateful logic.

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

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const value  = ref(stored !== null ? JSON.parse(stored) : defaultValue)

  watch(value, (v) => {
    localStorage.setItem(key, JSON.stringify(v))
  }, { deep: true })

  return value
}
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const language = useLocalStorage('lang', 'en')
</script>

Component Communication

Props + emits: parent passes data down; child emits events up.

<!-- Child.vue -->
<script setup>
const props = defineProps({ value: Number })
const emit  = defineEmits(['update:value', 'submit'])
</script>
<template>
  <input :value="props.value" @input="emit('update:value', +$event.target.value)" />
</template>

v-model on a component is shorthand for :modelValue + @update:modelValue. You can use multiple v-model bindings in Vue 3: v-model:title="...".

Provide / Inject: ancestor provides a value; any descendant can inject it without prop drilling.

// Parent
provide('config', readonly(reactive({ theme: 'dark' })))

// Child (any depth)
const config = inject('config')

Wrap provided values in readonly to prevent mutation from the consumer side.

Pinia — State Management

Pinia is the official Vue state management library. Each store is an independent module.

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const current  = ref(null)
  const isLoaded = ref(false)

  async function load(id) {
    current.value = await fetchUser(id)
    isLoaded.value = true
  }

  function logout() {
    current.value = null
  }

  return { current, isLoaded, load, logout }
})
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
userStore.load(1)
</script>
<template>
  <p v-if="userStore.isLoaded"></p>
</template>

Pinia stores are reactive — destructuring loses reactivity. Use storeToRefs(store) to destructure refs safely.


🔴 Senior

Reactivity System Internals

Vue 3’s reactivity is built on Proxy. When you access a property inside a reactive effect (computed, watch, template render), Vue tracks it via track(). When you mutate it, Vue triggers all dependent effects via trigger().

A simplified mental model:

const targetMap = new WeakMap()

function track(target, key) {
  // Record that the current active effect depends on target[key]
}

function trigger(target, key) {
  // Re-run all effects that depend on target[key]
}

This is why plain object properties inside reactive() are tracked, but properties added after initialization on a raw object are not — Proxy intercepts get and set on the proxy, but a raw object reference bypasses the proxy.

shallowRef / shallowReactive and Performance

reactive() deeply proxies nested objects. For large data structures (thousands of items), this deep conversion is expensive. Use shallowRef or shallowReactive when you only need top-level reactivity.

const bigList = shallowRef([]) // replacing the array triggers reactivity
// but mutating bigList.value[0].name does NOT trigger reactivity

// To update:
bigList.value = [...bigList.value, newItem]  // new reference — triggers

Teleport and Suspense

<Teleport> renders a subtree into a different DOM node (useful for modals that should be direct children of <body> to avoid stacking context issues).

<Teleport to="body">
  <div class="modal" v-if="open">
    <slot />
  </div>
</Teleport>

<Suspense> waits for async setup components or defineAsyncComponent to resolve, showing a fallback during loading.

<Suspense>
  <template #default>
    <AsyncUserProfile />  <!-- defineAsyncComponent or async setup -->
  </template>
  <template #fallback>
    <Skeleton />
  </template>
</Suspense>

Server-Side Rendering with Nuxt

Nuxt 3 is Vue’s meta-framework (like Next.js for React). Key concepts:

useFetch and useAsyncData run on the server during SSR and hydrate the client with the same data — no double fetch.

<script setup>
const { data: posts } = await useFetch('/api/posts')
</script>

File-based routing: pages/users/[id].vue maps to /users/:id. Server routes go in server/api/.

Senior Gotchas

Never destructure reactive() directly — the destructured variables are plain values, not refs. Use toRefs(state) to preserve reactivity.

ref inside reactive is automatically unwrapped — state.count instead of state.count.value. This can be surprising if you later extract the ref separately.

v-if and v-for on the same element: v-if takes priority in Vue 3. In Vue 2 it was the opposite. Always put them on separate elements to avoid ambiguity.

Watchers are lazy by default — they don’t run on initial mount. Use { immediate: true } to run immediately. watchEffect always runs immediately.

Components registered globally (via app.component) are available everywhere but cannot be tree-shaken. Prefer local imports.