Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
Vue.js
Vue.js là progressive JavaScript framework để xây dựng UI.
- So với React: Vue có template syntax rõ ràng hơn, two-way binding tích hợp sẵn, learning curve thấp hơn; React dùng JSX, unidirectional flow, hệ sinh thái lớn hơn
- So với Angular: Vue nhẹ hơn, ít opinionated hơn; Angular là full framework với DI, OOP, TypeScript bắt buộc
- Progressive: có thể dùng để add vào một phần app hoặc build full SPA
Options API (Vue 2 style): tổ chức code theo loại (data, methods, computed, watch).
Composition API (Vue 3): tổ chức code theo logic concern, dùng setup() hoặc <script setup>.
- Composition API dễ tái sử dụng logic hơn qua composables
- TypeScript support tốt hơn trong Composition API
- Options API vẫn được hỗ trợ đầy đủ trong Vue 3
Khuyến nghị dùng Composition API với <script setup> cho project mới.
<script setup> là syntactic sugar cho Composition API — tất cả code bên trong tự động expose ra template mà không cần return.
- Gọn hơn: không cần
export default, không cầnreturn - Hiệu năng tốt hơn: compiler tối ưu hóa tốt hơn
- TypeScript integration tốt hơn
defineProps,defineEmits,defineExposethay cho options props/emits
Pitfall: biến và function trong <script setup> khác file KHÔNG cần export — tự exposed.
Vue directives là các HTML attribute đặc biệt gắn kết reactive behavior vào DOM.
- Phổ biến nhất:
v-if/v-else/v-else-if: điều kiện render, xóa/tạo DOM thật.v-show: ẩn bằngdisplay:none, DOM vẫn tồn tại.v-for: loop render, luôn cần:key.v-model: two-way binding cho input.v-bind(:attr): bind attribute động.v-on(@event): gắn event listener.
Pitfall: v-if và v-for không nên dùng trên cùng element — v-if ưu tiên cao hơn v-for trong Vue 3 (ngược với Vue 2), dùng <template v-for> bọc bên ngoài.
v-if: xóa hoàn toàn DOM khi false — chi phí cao khi toggle thường xuyên, nhưng không render child khi không cần (lazy). v-show: chỉ toggle display:none — DOM luôn được render, chi phí thấp khi toggle.
- Dùng
v-showkhi element cần toggle thường xuyên. - Dùng
v-ifkhi điều kiện ít thay đổi, hoặc khi child component có side-effects cần tránh khởi tạo.
Vue components giao tiếp qua nhiều cơ chế tùy quan hệ và chiều dữ liệu.
- Props (parent → child): dữ liệu đi xuống, one-way
- Emits (child → parent): event đi lên, gọi
emit() - v-model: two-way binding, kết hợp props + emits
- provide/inject: ancestor → descendant, bỏ qua intermediaries
- Pinia (recommended) hoặc Vuex (legacy): global state store cho app-wide state
- Event bus (ít dùng trong Vue 3):
mittlibrary
Pitfall: tránh emit từ child để trực tiếp modify prop của parent — luôn emit event để parent tự update.
Vue Router là official router cho Vue.js — quản lý navigation, URL history, route matching.
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/user/:id', component: User },
{ path: '/:pathMatch(.*)*', component: NotFound },
]
})Navigate: <RouterLink to="/about"> trong template, hoặc router.push('/about') trong script. <RouterView /> là nơi component được render.
Pinia là official state management library cho Vue 3 — thay thế Vuex.
Ưu điểm:
- Composition API-friendly, không cần mutations
- TypeScript support tốt hơn nhiều
- Không có nested modules, mỗi store là một module độc lập
- Bundle nhỏ hơn (~1KB)
- Devtools support, hot-reload
- Server-side rendering support. Vuex 4 (dành cho Vue 3) hiện ở maintenance-only mode; Vuex 5 đã bị hủy — Pinia là official successor
SFC (.vue file) gom template, script, style vào một file duy nhất — tiện lợi cho development.
Cấu trúc: <template> (HTML), <script setup> (logic), <style scoped> (CSS).
scopedattribute: CSS chỉ áp dụng cho component đó, tránh conflict- Compiler (Vite/webpack) parse và compile SFC thành JavaScript
- Hỗ trợ lang attribute:
<script lang="ts">,<style lang="scss">
Pitfall: scoped CSS không ảnh hưởng lên child components — dùng :deep() nếu cần.
Vue dùng :key để identify mỗi vnode khi diff — giúp tái sử dụng và reorder DOM nodes đúng cách thay vì re-render toàn bộ.
- Thiếu key: Vue dùng "in-place patch" — có thể gây lỗi với stateful components hoặc animation
- Dùng index làm key: không nên khi list có thể bị sort/filter — index thay đổi gây re-render sai
- Dùng unique stable ID (e.g.,
item.id) là best practice
v-model là shorthand: :modelValue + @update:modelValue.
Mặc định cho input:
<!-- Tương đương -->
<input v-model="name" />
<input :value="name" @input="name = $event.target.value" />Custom component v-model:
<script setup>
// Child component
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
// Template: :value="modelValue" @input="emit('update:modelValue', $event)"
</script>Vue 3 hỗ trợ multiple v-model: v-model:title, v-model:content với props tương ứng.
Vue 3 dùng ES6 Proxy để intercept get/set operations trên reactive objects.
Khi đọc property trong effect (computed, watcher, render): dependency được track.
Khi set property: trigger cập nhật tất cả dependents.
Cải tiến so với Vue 2 (dùng Object.defineProperty):
- Detect thêm/xóa property động
- Detect array index changes và
.length - Lazy — không cần walk toàn bộ object tree upfront
ref: wrap bất kỳ giá trị nào (primitives, objects) thành reactive container — truy cập qua .value trong script, tự unwrap trong template.reactive: wrap object/array thành reactive Proxy — truy cập trực tiếp, không cần .value.
Khuyến nghị hiện tại (Vue 3.2+): Dùng ref cho tất cả — kể cả objects. Vue core team đã cập nhật docs ưu tiên ref universally vì reactive có gotchas: mất reactivity khi destructure, mất reactivity khi re-assign cả object. reactive chỉ nên dùng khi cần nhóm nhiều state liên quan.
Pitfall: destructure từ reactive sẽ mất reactivity — dùng toRefs():
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // giữ reactivitycomputed: derive giá trị từ reactive state, kết quả được cache, chỉ recompute khi dependency thay đổi — dùng để transform/calculate data cho template.
watch: observe cụ thể một hoặc vài sources, có access vào old/new value, lazy by default — dùng khi cần side effect (API call, DOM manipulation) khi data thay đổi.
watchEffect: auto-track tất cả reactive dependencies dùng bên trong, chạy ngay khi mount — dùng khi không cần old value và muốn auto-dependency detection.
Composable là function dùng Composition API để đóng gói và tái sử dụng stateful logic.
Ví dụ:
// useFetch.js
export function useFetch(url) {
const data = ref(null)
const loading = ref(true)
fetch(url).then(r => r.json()).then(d => { data.value = d; loading.value = false })
return { data, loading }
}So với Mixins:
- Không có naming collision — return value rõ ràng
- Source rõ ràng — biết data từ đâu
- Có thể nhận arguments (dynamic)
- Không có implicit state sharing
Mixins vẫn được hỗ trợ trong Vue 3 nhưng bị discouraged — Composition API là alternative được khuyến nghị.
Trong <script setup>, lifecycle hooks được import và dùng như functions:
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue'
onMounted(() => { console.log('mounted') })
onUnmounted(() => { /* cleanup */ })Mapping từ Options API (bao gồm Vue 2 → Vue 3): beforeCreate/created → code trong setup() chạy thay thế, mounted → onMounted, updated → onUpdated, beforeUpdate → onBeforeUpdate, unmounted → onUnmounted, beforeMount → onBeforeMount.
- Vue 2:
beforeDestroy→onBeforeUnmount,destroyed→onUnmounted.
Pitfall: onMounted trong SSR (Nuxt) không chạy server-side — dùng cho browser-only code.
provide / inject cho phép truyền data qua component tree mà không cần props drilling.
Parent provide, bất kỳ descendant nào có thể inject:
// Parent
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
// Child (bất kỳ cấp)
import { inject } from 'vue'
const theme = inject('theme', 'light') // 'light' là defaultDùng khi:
- Shared state cho subtree (theme, locale, auth)
- Plugin/library cung cấp context
Pitfall: khó debug hơn props vì data flow không explicit — dùng Symbol key để tránh naming collision.
defineProps khai báo props mà component nhận, defineEmits khai báo events mà component emit:
// TypeScript style (recommended)
const props = defineProps<{
title: string
count?: number
}>()
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'close'): void
}>()
// Dùng
emit('update', 42)Pitfall: defineProps không thể destructure trực tiếp mà giữ reactivity trong Vue 3.4 trở về trước — dùng toRefs(props).
Từ Vue 3.5+: destructure props với const { title } = defineProps() giữ reactivity.
Slots cho phép parent inject content vào template của child component.
- Default slot:
<slot />. - Named slots:
<slot name="header" />— parent dùng<template #header>. - Scoped slot: child truyền data lên parent qua slot:
<!-- Child -->
<slot :item="item" :index="i" />
<!-- Parent -->
<template #default="{ item, index }">
<span>{{ index }}: {{ item.name }}</span>
</template>Dùng scoped slots khi child biết cách lấy data nhưng parent quyết định cách render (render prop pattern).
Pitfall: không mix slot và v-if trên cùng <template> — tách riêng.
Trong <script setup>, component instance mặc định không expose properties ra ngoài (không access được qua template ref). defineExpose cho phép explicitly expose các methods/properties để parent gọi qua ref.
Ví dụ: defineExpose({ focus, reset }) — parent gọi childRef.value.focus(). Dùng khi cần imperative control (focus, scroll, reset form). Pitfall: không expose quá nhiều — tránh biến component thành "god object", ưu tiên event-based communication.
Navigation guards cho phép control navigation — xác thực, redirect, cancel.
// Global guard
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
return { name: 'Login' } // redirect
}
})
// Per-route guard
{ path: '/admin', component: Admin, beforeEnter: (to, from) => { ... } }In-component guards (Options API): beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave.
- Trong Composition API dùng
onBeforeRouteLeave,onBeforeRouteUpdate. - Return
falseđể cancel, return route location để redirect, returnundefined/trueđể proceed.
Dynamic segment: path: '/user/:id' — đọc qua route.params.id.
Lazy loading với dynamic import:
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
},
{
path: '/user/:id',
// Với Vite: không cần webpackChunkName; tên chunk cấu hình qua build.rollupOptions
component: () => import('./views/User.vue')
}
]- Lazy loading: component chỉ được download khi navigate đến route đó — giảm initial bundle size.
- Lưu ý:
/ webpackChunkName /là Webpack magic comment, không có tác dụng trong Vite.
Pitfall: đọc route.params.id trong <script setup>: dùng const route = useRoute().
Pinia stores được tạo với defineStore() dùng setup-store syntax (khuyến nghị), trả về refs cho state, computed getters, và action functions.
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
function reset() { count.value = 0 }
return { count, double, increment, reset }
})// Trong component
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
store.increment()
console.log(store.count, store.double)Pitfall: destructure store mất reactivity — dùng storeToRefs: const { count, double } = storeToRefs(store) (methods thì destructure thường).
Pinia store là reactive object — destructuring trực tiếp sẽ mất reactivity (tương tự reactive()). storeToRefs() convert state và getters thành refs để destructure an toàn: const { count, name } = storeToRefs(store).
- Methods (actions) không cần
storeToRefs— destructure thường:const { increment } = store.
Pitfall: nếu dùng storeToRefs với methods, chúng trở thành refs — không gọi được như function.
v-memo skip re-render một subtree nếu array dependency không thay đổi — tương tự React.memo nhưng ở template level:
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<!-- Chỉ re-render khi id hoặc selected thay đổi -->
<ExpensiveComponent :item="item" />
</div>Dùng cho:
- Long lists với expensive child renders
- Chỉ một vài properties ảnh hưởng đến render
Pitfall: không lạm dụng — dependency array phải đầy đủ, thiếu dependency sẽ gây stale render.
<KeepAlive> cache component instance khi unmount — state được giữ nguyên khi switch lại:
<KeepAlive :include="['FormStep1', 'FormStep2']" :max="5">
<component :is="currentTab" />
</KeepAlive>- Lifecycle hooks cho cached components:
onActivated(khi cache hit, component show lại),onDeactivated(khi bị hide, không unmount). - Dùng cho: tab views, multi-step forms, expensive components cần giữ state.
Pitfall: cached components vẫn chiếm bộ nhớ — dùng :max để limit, tránh cache tất cả.
Dùng dynamic import để lazy load component — chỉ download khi cần:
import { defineAsyncComponent } from 'vue'
// Basic
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// Với loading/error states
const AsyncComp = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // delay trước khi show loading
timeout: 3000, // timeout sau 3s
})Kết hợp với <Suspense> để handle loading state elegantly.
Vue cung cấp error boundary mechanism qua errorCaptured (Options API) / onErrorCaptured (Composition API):
// App.vue — global error boundary
import { onErrorCaptured } from 'vue'
onErrorCaptured((err, instance, info) => {
console.error('Caught:', err, info)
// Return false để prevent propagation
return false
})- Global handler:
app.config.errorHandler = (err, instance, info) => { ... }. - Error types caught: lifecycle hooks, event handlers, async errors trong setup, child component errors.
Pitfall: không catch errors trong async callbacks không thuộc Vue (setTimeout, fetch handlers) — cần try/catch thủ công.
Vue components được test bằng cách mount chúng với @vue/test-utils và assert trên rendered output và emitted events, dùng Vitest làm test runner.
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('increments when button clicked', async () => {
const wrapper = mount(Counter, {
props: { initialCount: 0 }
})
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
expect(wrapper.emitted('update')).toBeTruthy()
})
it('renders slot content', () => {
const wrapper = mount(Counter, {
slots: { default: '<span>Label</span>' }
})
expect(wrapper.find('span').exists()).toBe(true)
})
})Dùng shallowMount để stub child components.
Test behavior, không test implementation details.
Vue batch DOM updates — không update ngay khi state thay đổi, mà queue updates và flush async. nextTick cho phép chờ DOM được update xong:
import { nextTick, ref } from 'vue'
const count = ref(0)
async function handleClick() {
count.value++
// DOM chưa update ở đây
await nextTick()
// DOM đã update — giờ có thể đọc DOM hoặc scroll
console.log(document.querySelector('.count')?.textContent)
}Dùng khi:
- Cần đọc DOM sau khi state update
- Focus element sau khi v-if toggle
- Scroll sau khi append item vào list
toRef: tạo ref từ một property của reactive object — giữ reactive connection:
const state = reactive({ count: 0, name: 'Vue' })
const countRef = toRef(state, 'count') // linked to state.counttoRefs: convert toàn bộ reactive object thành object of refs — dùng khi destructure:
const { count, name } = toRefs(state)toValue (Vue 3.3+): unwrap ref hoặc getter — dùng trong composables để accept cả ref và plain value:
function useFeature(id: MaybeRefOrGetter<string>) {
const resolvedId = toValue(id) // unwrap nếu là ref
}Vue a11y best practices theo chuẩn ARIA và semantic HTML, kết hợp Vue-specific tooling như vue-axe và eslint-plugin-vuejs-accessibility để phát hiện vấn đề sớm.
- Dùng semantic HTML trong templates (
<button>không phải<div @click>) :aria-label,:aria-expanded,:aria-livevới dynamic content- Focus management với
nextTickvàel.focus()sau modal open/close v-bind="$attrs"để inherit aria attrsaxe-corehoặcvue-axeplugin để audit a11y trong dev- Keyboard navigation — test với Tab, Enter, Esc
- Color contrast ratio ≥ 4.5:1 cho text
Dùng eslint-plugin-vuejs-accessibility để lint a11y issues.
Template ref cho phép truy cập DOM element hoặc component instance trực tiếp:
<script setup>
import { ref, onMounted } from 'vue'
// Ref cho DOM element
const inputEl = ref<HTMLInputElement | null>(null)
// Ref cho component instance
const modalRef = ref<InstanceType<typeof Modal> | null>(null)
onMounted(() => {
inputEl.value?.focus() // Auto-focus khi mount
})
function openModal() {
modalRef.value?.open() // Gọi method của child component
}
</script>
<template>
<input ref="inputEl" type="text" />
<Modal ref="modalRef" />
</template>Pitfall: ref.value là null trước khi component mount — luôn kiểm tra trong onMounted hoặc dùng optional chaining ref.value?.method().
Pattern chuẩn để fetch async data trong Vue 3 Composition API là dùng onMounted với try/catch/finally, hoặc trích xuất thành composable tái sử dụng.
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
const loading = ref(true)
const error = ref(null)
// Pattern 1: onMounted (phổ biến nhất)
onMounted(async () => {
try {
const res = await fetch('/api/users')
users.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
// Pattern 2: Composable reusable
// useFetch.ts — tái sử dụng ở nhiều components
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="u in users" :key="u.id">{{ u.name }}</li>
</ul>
</template>Pitfall: không gọi async setup() trực tiếp mà không có <Suspense> bọc ngoài — dùng onMounted hoặc composable thay.
Với Nuxt: dùng useFetch() / useAsyncData() để SSR-compatible.
Vue tự động unwrap refs trong một số contexts — hiểu rõ để tránh bugs:
Template auto-unwrap: refs được auto-unwrap trong template, không cần .value:
<script setup>
const count = ref(0)
const state = reactive({ count: ref(0) })
</script>
<template>
<!-- Tự động unwrap — không cần .value -->
<p>{{ count }}</p>
<p>{{ state.count }}</p>
</template>Reactive object unwrap: ref là property của reactive object được tự động unwrap khi access:
const count = ref(0)
const state = reactive({ count }) // Wrap ref trong reactive
console.log(state.count) // 0, không phải Ref<0> — auto-unwrap!
state.count++ // Tương đương count.value++
console.log(count.value) // 1 — linked!Không unwrap: ref trong array hoặc Map KHÔNG tự động unwrap:
const list = reactive([ref(0)])
console.log(list[0].value) // Phải dùng .value!
const map = reactive(new Map([['key', ref(0)]]))
console.log(map.get('key').value) // Phải dùng .value!Route meta cho phép đính kèm custom data vào routes — thường dùng cho auth, breadcrumbs, page titles:
// router/index.ts — declare meta type
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
title?: string
breadcrumb?: string
}
}
const routes = [
{
path: '/admin',
component: AdminLayout,
meta: { requiresAuth: true, roles: ['admin'], title: 'Admin Panel' },
children: [
{
path: 'users',
component: UsersView,
meta: { breadcrumb: 'Users Management' },
}
]
},
]
// Global guard dùng meta
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
// Component access
const route = useRoute()
console.log(route.meta.title) // Type-safe nhờ declaration mergingPitfall: to.meta tổng hợp metadata từ tất cả matched routes (parent + child) — child ghi đè parent.
Vue Router cho phép kiểm soát scroll behavior khi navigate:
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// savedPosition: vị trí scroll trước đó khi dùng browser back/forward
if (savedPosition) {
return savedPosition // Restore khi back/forward
}
// Scroll đến named anchor
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth',
top: 80, // Offset cho fixed header
}
}
// Scroll về đầu trang khi navigate thường
return { top: 0, behavior: 'smooth' }
},
})
// Async scroll — chờ transition xong
scrollBehavior(to, from, savedPosition) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0, behavior: 'smooth' })
}, 300) // Chờ page transition 300ms
})
}Dùng savedPosition để implement proper back/forward scroll restoration — trải nghiệm native-like.
Dùng InjectionKey để provide/inject type-safe, tránh string key collisions:
// keys.ts — export typed keys
import type { InjectionKey, Ref } from 'vue'
export interface UserContext {
id: number
email: string
role: string
}
export const userContextKey: InjectionKey<UserContext> = Symbol('userContext')
export const themeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')
// Parent component
import { provide, ref } from 'vue'
import { userContextKey, themeKey } from './keys'
const theme = ref<'light' | 'dark'>('dark')
provide(themeKey, theme)
provide(userContextKey, { id: 1, email: 'user@test.com', role: 'admin' })
// Child component — fully typed
import { inject } from 'vue'
import { themeKey, userContextKey } from './keys'
const theme = inject(themeKey) // Ref<'light' | 'dark'> | undefined
const user = inject(userContextKey) // UserContext | undefined
// Với default value — loại bỏ undefined
const theme = inject(themeKey, ref('light')) // Ref<'light' | 'dark'>Dùng Symbol làm key thay vì string để tránh naming collision trong large apps hoặc libraries.
v-once: render element/component một lần duy nhất — skip future updates.
Sau khi render, element được xử lý như static content:
<!-- Static content không bao giờ thay đổi -->
<h1 v-once>{{ appName }}</h1>
<!-- Expensive initial render, không bao giờ update -->
<StaticBanner v-once :data="bannerConfig" />
<!-- Kết hợp với v-for — freeze sau render đầu -->
<div v-for="item in list" :key="item.id" v-once>
{{ item.name }}
</div>v-pre: skip compilation toàn bộ subtree — dùng cho content muốn hiển thị mustache syntax như raw text:
<!-- Hiển thị '{{ this is NOT compiled }}' -->
<span v-pre>{{ this is NOT compiled }}</span>
<!-- Dùng trong docs/code display components -->
<code v-pre>const x = ref(0)</code>Khác nhau: v-once render rồi freeze (tốt cho performance). v-pre skip compile entirely (tốt cho mustache syntax display).
Pitfall: v-once trong component vẫn chạy setup() và reactive setup — chỉ DOM update bị skip.
Built-in modifiers:
- .lazy: sync sau change event thay vì input — giảm updates
- .number: auto convert sang number (tương đương parseFloat)
- .trim: auto trim whitespace
<input v-model.lazy="search" /> <!-- Update khi blur/enter -->
<input v-model.number="age" type="number" />
<input v-model.trim="name" />Custom modifiers cho component v-model:
// Child component
const props = defineProps<{
modelValue: string
modelModifiers?: { uppercase?: boolean; capitalize?: boolean }
}>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
function handleInput(e: Event) {
let value = (e.target as HTMLInputElement).value
if (props.modelModifiers?.uppercase) value = value.toUpperCase()
if (props.modelModifiers?.capitalize) value = value.charAt(0).toUpperCase() + value.slice(1)
emit('update:modelValue', value)
}<!-- Parent usage -->
<MyInput v-model.uppercase="text" />
<MyInput v-model.capitalize="text" />
<!-- Multiple v-model với modifiers -->
<MyInput v-model:title.capitalize="title" v-model:content="content" />Dùng plugin pinia-plugin-persistedstate:
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// store
export const useAuthStore = defineStore('auth', () => {
const token = ref('')
return { token }
}, {
persist: {
storage: localStorage,
pick: ['token'], // chỉ persist token
}
})Pitfall: không persist sensitive data trong localStorage (dễ bị XSS đọc).
Dùng sessionStorage hoặc HTTP-only cookies cho auth tokens.
<Teleport> render content vào một DOM node nằm ngoài component tree — nhưng vẫn là con về mặt logic (data flow, events hoạt động bình thường):
<Teleport to="body">
<div class="modal-overlay" v-if="showModal">
<div class="modal">...</div>
</div>
</Teleport>Use cases:
- Modals, drawers, tooltips — tránh z-index/overflow issues
- Notifications/toasts — render ở root level
- Bất kỳ UI cần break khỏi parent overflow/stacking context
Pitfall: Teleport content vẫn share reactive state với parent component — props, emits, provide/inject hoạt động bình thường.
<Suspense> cho phép render fallback content trong khi async component đang resolve — xử lý async setup():
<Suspense>
<template #default>
<AsyncDashboard /> <!-- async setup() -->
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>AsyncDashboard có thể có async setup() với await bên trong. <Suspense> catch async và show fallback cho đến khi resolve.
- Tích hợp với
<KeepAlive>và lazy components.
Pitfall: Một số edge case (nhiều async deps, nested Suspense với SSR) có thể behave không như kỳ vọng — test kỹ.
- Tag "experimental" đã được xóa khỏi Vue 3 docs.
Custom directive cho phép reuse DOM manipulation logic.
Trong <script setup>:
// vFocus — auto-focus element khi mount
const vFocus = {
mounted: (el) => el.focus()
}
// vTooltip với value
const vTooltip = {
mounted(el, binding) {
el.title = binding.value
el.style.cursor = 'help'
},
updated(el, binding) {
el.title = binding.value
}
}<input v-focus />
<span v-tooltip="'Hover me'">?</span>Directive hooks: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted.
Vue 3 mang lại nhiều cải tiến căn bản về API và performance so với Vue 2.
- Composition API +
<script setup>— thay thế Options API (vẫn supported) - Reactivity dùng Proxy thay Object.defineProperty — detect thêm/xóa property
- Fragments, Teleport, Suspense — component mới
- Multiple v-model trên component
createApp()thaynew Vue()— tách instance tốt hơn- TypeScript support tốt hơn
- Vue CLI (Webpack-based) bị deprecated; scaffolding hiện tại (
npm create vue@latest) dùng Vite - Pinia thay Vuex
- Bundle nhỏ hơn nhờ tree-shaking tốt hơn
- v-if ưu tiên hơn v-for (ngược Vue 2)
Nuxt.js là meta-framework cho Vue với SSR/SSG built-in. SSR flow:
- Request đến server
- Nuxt render Vue app thành HTML string
- HTML được gửi cho client
- Client hydration — Vue attach event listeners lên server-rendered HTML
Lợi ích: SEO tốt hơn, faster First Contentful Paint. Pitfall:
window/documentkhông có ở server — wrap trongonMountedhoặc checkimport.meta.client(Nuxt 3 idiom;process.clientlà Nuxt 2 legacy)- State mismatch giữa server và client gây hydration error
onMountedkhông chạy server-side
Render function thay template bằng JavaScript thuần — linh hoạt hơn cho dynamic rendering:
import { h, defineComponent } from 'vue'
export default defineComponent({
props: ['tag', 'content'],
render() {
return h(this.tag || 'div', { class: 'dynamic' }, this.content)
}
})
// Trong <script setup> với useSlots()
import { h, useSlots } from 'vue'
const slots = useSlots()
// return () => h('div', slots.default?.())Dùng khi:
- Component cần tag động không thể express qua template
- Library code cần flexibility
- Tái sử dụng render logic phức tạp
Template vẫn được khuyến nghị cho business logic thông thường.
<Transition> apply CSS classes khi element enter/leave:
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }<TransitionGroup> cho list animations — thêm move class:
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>Modes: mode="out-in" (old leaves trước, new enters sau) — tránh flickering khi swap components.
Vue 3 performance tập trung vào giảm unnecessary reactivity và re-renders qua directives, component patterns, và build tooling.
v-memocho long lists với expensive rendersshallowRef/shallowReactivecho large data structures không cần deep reactivity- Lazy load routes và components với dynamic import
<KeepAlive>cho frequently toggled components- Dùng
computedthaymethodsđể cache kết quả - Tránh unnecessary watchers — prefer computed
v-oncecho content không thay đổi- Tránh inline handlers phức tạp trong template
- Tree-shaking — import chỉ những gì cần từ Vue
- Analyze bundle với
rollup-plugin-visualizer
Hydration mismatch xảy ra khi HTML do server render khác với những gì Vue client muốn render — Vue sẽ warning và re-render toàn bộ component.
Nguyên nhân phổ biến:
1. Browser-only APIs trong setup: localStorage, window, document không tồn tại trên server
2. Random/Date values: Math.random(), Date.now(), new Date() cho kết quả khác nhau
3. Conditional dựa trên user agent/cookies: server không có context này
4. Third-party directives chỉ chạy client-side
Fixes:
// 1. Dùng <ClientOnly> component trong Nuxt
<ClientOnly fallback-tag="span">
<ComponentThatUsesWindow />
</ClientOnly>
// 2. import.meta.client (Nuxt 3 idiom; process.client là Nuxt 2 legacy)
const count = ref(import.meta.client ? localStorage.getItem('count') : null)
// 3. onMounted cho browser-only code
onMounted(() => {
// Safe — only runs on client
state.value = localStorage.getItem('key')
})Lưu ý: Vue/Nuxt không có suppressHydrationWarning attribute (đó là React).
Để intentional mismatch dùng <ClientOnly> hoặc :key trick để force re-render.
Nuxt 3 cung cấp 3 data-fetching primitives: useFetch (tiện lợi, auto-keyed), useAsyncData (linh hoạt, multi-source), và $fetch (raw, cho user-triggered actions).
useFetch: wrapper tiện lợi nhất — tự động deduplicate, cache key từ URL:
// Chạy cả server lẫn client, SSR-compatible
const { data, pending, error, refresh } = await useFetch('/api/users', {
lazy: false, // true = không chặn navigation
server: true, // false = chỉ fetch client-side
transform: (data) => data.users,
})useAsyncData: flexible hơn cho custom async logic:
const { data } = await useAsyncData('unique-key', async () => {
const [users, posts] = await Promise.all([
$fetch('/api/users'),
$fetch('/api/posts'),
])
return { users, posts }
})$fetch: raw fetch (ofetch under the hood) — không tự cache hay deduplicate:
// Dùng trong event handlers, actions — không trong setup
async function submitForm() {
const result = await $fetch('/api/users', {
method: 'POST', body: formData.value
})
}Rule of thumb: setup/navigation data → useFetch/useAsyncData.
User-triggered actions → $fetch.
shallowRef, shallowReactive, markRaw — khi nào dùng để tối ưu?Deep reactivity có overhead — Vue track tất cả nested properties. Các APIs này cho phép opt-out:
shallowRef: chỉ track .value thay đổi, không deep-track nội dung:
// Dùng cho large objects không cần nested reactivity
const bigData = shallowRef({ /* 1000 properties */ })
bigData.value = newData // Trigger update
bigData.value.name = 'new' // KHÔNG trigger updateshallowReactive: tương tự cho object — chỉ track top-level properties:
const state = shallowReactive({ user: { name: 'Alice' }, list: [] })
state.user = newUser // Trigger update
state.user.name = 'Bob' // KHÔNG trigger updatemarkRaw: đánh dấu object không bao giờ bị reactive — dùng cho third-party instances:
import { markRaw } from 'vue'
const chartInstance = markRaw(new Chart(canvas, config))
const state = reactive({ chart: chartInstance }) // chart không bị Proxy wrapDùng khi: large data structures không cần deep reactivity, third-party class instances (Chart.js, mapbox), performance-critical list items.
Pinia plugins cho phép extend tất cả stores — thêm properties, wrap actions, subscribe to changes:
import { PiniaPluginContext } from 'pinia'
// Plugin thêm $reset cho tất cả stores (setup stores không có built-in reset)
function resetPlugin({ store, options }: PiniaPluginContext) {
// Lưu initial state
const initialState = JSON.parse(JSON.stringify(store.$state))
store.$reset = () => {
store.$patch(initialState)
}
}
// Plugin logging actions
function logPlugin({ store }: PiniaPluginContext) {
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Store: ${store.$id}] Action: ${name}`, args)
after((result) => console.log('Result:', result))
onError((error) => console.error('Error:', error))
})
}
// Register plugins
const pinia = createPinia()
pinia.use(resetPlugin)
pinia.use(logPlugin)
// Usage
const store = useCounterStore()
store.$reset() // Available từ pluginUse cases: logging, error tracking (Sentry), optimistic updates, undo/redo, sync với localStorage, add $api property.
watchEffect deep dive — cleanup, flush timing và stop watcher?watchEffect tự động track dependencies và re-run khi chúng thay đổi.
Có các tùy chọn nâng cao:
import { watchEffect, ref } from 'vue'
const userId = ref(1)
const userData = ref(null)
// Cleanup function — cancel previous async operation
const stop = watchEffect(async (onCleanup) => {
let cancelled = false
onCleanup(() => {
cancelled = true // Được gọi trước khi effect chạy lại
})
const data = await fetch(`/api/users/${userId.value}`).then(r => r.json())
if (!cancelled) {
userData.value = data // Chỉ update nếu request này vẫn relevant
}
})
// Stop watcher thủ công
stop() // Dừng watching — cleanup memory leak
// Flush timing
watchEffect(() => {
// Mặc định: pre-flush — chạy trước DOM update
}, { flush: 'post' }) // post: chạy sau DOM update
// watchPostEffect và watchSyncEffect — syntactic sugar
import { watchPostEffect, watchSyncEffect } from 'vue'
watchPostEffect(() => { /* chạy sau DOM update */ })Dùng cleanup cho: cancel fetch requests, clear timers, cancel WebSocket subscriptions khi dependency thay đổi.
Render hàng nghìn DOM nodes cùng lúc rất chậm. Virtual scrolling chỉ render visible items:
Vue Virtual Scroller (@vueuse/components hoặc vue-virtual-scroller):
<script setup>
import { useVirtualList } from '@vueuse/core'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })))
const { list, containerProps, wrapperProps } = useVirtualList(items, {
itemHeight: 40, // Fixed height per item (required)
})
</script>
<template>
<!-- Container với overflow scroll -->
<div v-bind="containerProps" style="height: 600px; overflow-y: auto">
<!-- Wrapper với total height để scroll bar đúng -->
<div v-bind="wrapperProps">
<!-- Chỉ visible items được render -->
<div
v-for="{ data: item } in list"
:key="item.id"
style="height: 40px"
>
{{ item.name }}
</div>
</div>
</div>
</template>Với variable-height items, dùng vue-virtual-scroller's DynamicScroller.
Kết hợp với v-memo cho items có nhiều reactive dependencies.
Nuxt 3 có hai loại middleware khác nhau:
Route Middleware (client + server): chạy khi navigating, kiểm soát routing:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { loggedIn } = useAuth()
if (!loggedIn.value && to.meta.requiresAuth) {
return navigateTo('/login')
}
})
// Dùng trong page
// pages/dashboard.vue
definePageMeta({ middleware: 'auth' })
// Inline middleware
definePageMeta({
middleware: [
async (to) => {
if (!canAccess(to)) return abortNavigation()
}
]
})Server Middleware (server-only): chạy trên server trước mọi request, giống Express middleware:
// server/middleware/logger.ts
export default defineEventHandler((event) => {
console.log(`[${event.method}] ${event.path}`)
// Không return gì → tiếp tục pipeline
})
// server/middleware/rate-limit.ts
export default defineEventHandler(async (event) => {
const ip = getRequestIP(event)
const count = await redis.incr(`rate:${ip}`)
if (count > 100) {
throw createError({ statusCode: 429, message: 'Too Many Requests' })
}
})