26 mẹo phát triển Vue 3 hiệu quả


Tuan Duc Tran

Vue đã thay đổi đáng kể so với giai đoạn Vue 2 phổ biến vào năm 2019. Nhiều mẹo từng hữu ích vẫn còn giá trị, nhưng một số đã trở nên lỗi thời hoặc cần được diễn giải lại trong bối cảnh Vue 3, Composition API, Vite, Pinia, TypeScript và các công cụ hiện đại.

Bài viết này tổng hợp 26 mẹo phát triển Vue theo hướng thực tế cho năm 2026: giảm mã thừa, tránh các lỗi kiến trúc phổ biến, cải thiện khả năng bảo trì và sử dụng đúng công cụ thay vì chỉ dựa vào thói quen cũ.

1. Sử dụng Pinia thay vì bắt đầu dự án mới với Vuex

Trong các dự án Vue 3 mới, Pinia là lựa chọn phù hợp hơn Vuex.

Vuex vẫn có thể xuất hiện trong các dự án cũ, nhưng nếu bắt đầu một dự án mới, bạn nên ưu tiên:

  • Composables cho logic dùng chung đơn giản.
  • Pinia cho state dùng chung giữa nhiều màn hình hoặc nhiều tính năng.
  • Local state cho dữ liệu chỉ thuộc về một component.

Không phải mọi state đều cần đưa vào store.

Ví dụ, trạng thái mở hoặc đóng một modal chỉ được sử dụng trong một component thì không cần Pinia. Ngược lại, thông tin người dùng đăng nhập, giỏ hàng, quyền truy cập hoặc cấu hình ứng dụng thường phù hợp với store.

// stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
 
export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
 
  function setUser(value: User | null) {
    user.value = value
  }
 
  return {
    user,
    setUser,
  }
})

Nguyên tắc đơn giản:

Chỉ đưa state vào store khi nó thực sự cần được chia sẻ hoặc quản lý ở cấp ứng dụng.

2. Hiểu rõ component instance và Composition API

Trong Vue 2, nhiều logic thường xoay quanh this:

export default {
  data() {
    return {
      count: 0,
    }
  },
 
  methods: {
    increment() {
      this.count++
    },
  },
}

Trong Vue 3, Composition API giúp tổ chức logic theo tính năng thay vì chia nhỏ theo data, methods, computedwatch.

<script setup lang="ts">
import { computed, ref } from 'vue'
 
const count = ref(0)
 
const doubled = computed(() => count.value * 2)
 
function increment() {
  count.value++
}
</script>
 
<template>
  <button @click="increment">
    {{ count }} / {{ doubled }}
  </button>
</template>

Composition API giúp:

  • Giảm phụ thuộc vào this.
  • Dễ tái sử dụng logic.
  • Dễ kiểm thử.
  • Dễ tách logic phức tạp thành composable.
  • Phù hợp hơn với TypeScript.

Tuy nhiên, Options API không phải là sai. Nó vẫn phù hợp với component đơn giản hoặc codebase đang sử dụng Options API nhất quán.

Vấn đề không phải API nào tốt tuyệt đối, mà là tránh trộn lẫn nhiều phong cách một cách thiếu chủ đích.

3. Đừng ép component render lại nếu chưa hiểu nguyên nhân

Một số lập trình viên sử dụng các cách như:

key.value++

hoặc:

await nextTick()

chỉ để ép giao diện cập nhật.

Trong Vue, việc render lại thường được xử lý tự động thông qua hệ thống reactivity. Nếu bạn phải liên tục ép component render lại, rất có thể:

  • Dữ liệu không reactive.
  • Bạn đang thay đổi sai kiểu dữ liệu.
  • Bạn đang destructure reactive state không đúng cách.
  • Bạn đang giữ một giá trị đã bị mất liên kết reactive.
  • Component đang có thiết kế state không rõ ràng.
  • Bạn đang dùng DOM trực tiếp thay vì điều khiển dữ liệu.

Hãy tìm nguyên nhân trước khi dùng thủ thuật force update.

Việc dùng key để reset component đôi khi vẫn hợp lý, nhưng nên là quyết định có chủ đích, không phải cách chữa cháy mặc định.

4. Tận dụng Fragment và nhiều phần tử root

Vue 2 yêu cầu mỗi component phải có một phần tử root duy nhất:

<template>
  <div>
    <header />
    <main />
  </div>
</template>

Vue 3 hỗ trợ nhiều phần tử root:

<template>
  <header />
  <main />
  <footer />
</template>

Điều này giúp giảm các wrapper không cần thiết trong DOM.

Tuy nhiên, không nên loại bỏ mọi wrapper một cách máy móc. Một wrapper vẫn có thể cần thiết cho:

  • Layout.
  • Flexbox hoặc Grid.
  • Accessibility.
  • Quản lý spacing.
  • Grouping logic.
  • Transition.
  • CSS selector.

Mục tiêu không phải là có ít thẻ HTML nhất, mà là có cấu trúc DOM rõ ràng và có ý nghĩa.

5. Đừng tự xây hệ thống validation quá sớm

Validation form thường bắt đầu bằng vài điều kiện đơn giản:

if (!email.value) {
  errors.email = 'Email is required'
}

Nhưng khi form lớn dần, bạn có thể phải xử lý:

  • Required.
  • Email.
  • Min/max length.
  • Confirm password.
  • Async validation.
  • Validation theo điều kiện.
  • Hiển thị lỗi theo từng field.
  • Reset lỗi.
  • Validate khi blur hoặc submit.
  • Schema dùng chung giữa frontend và backend.

Lúc này, việc tự viết toàn bộ hệ thống validation có thể tạo ra nhiều mã lặp.

Bạn có thể cân nhắc:

  • VeeValidate.
  • Zod.
  • Valibot.
  • Vuelidate nếu dự án đã sử dụng nó.
  • Schema validation dùng chung với backend khi phù hợp.

Không nhất thiết phải dùng thư viện cho mọi form. Với form nhỏ, validation thủ công vẫn dễ đọc hơn. Nhưng với form phức tạp, schema-based validation thường giúp giảm đáng kể độ phức tạp.

6. Thiết kế component bằng contract rõ ràng

Một component tốt không chỉ là component có ít dòng code. Nó cần có contract rõ ràng:

  • Nhận props gì?
  • Emit event gì?
  • Có slot nào?
  • Giá trị mặc định là gì?
  • Trạng thái loading được xử lý thế nào?
  • Có hỗ trợ keyboard không?
  • Có thể tái sử dụng trong ngữ cảnh nào?
  • Component có phụ thuộc trực tiếp vào store hoặc route không?

Ví dụ:

<script setup lang="ts">
defineProps<{
  title: string
  disabled?: boolean
}>()
 
const emit = defineEmits<{
  submit: []
}>()
</script>

Một component nên có API bên ngoài đơn giản, dễ đoán và ổn định.

Tránh những component:

  • Nhận quá nhiều props.
  • Có quá nhiều event.
  • Tự gọi API không rõ ràng.
  • Tự đọc route, store và localStorage cùng lúc.
  • Có quá nhiều trách nhiệm.
  • Không thể sử dụng nếu thiếu một loạt context ngầm.

Nếu một component cần quá nhiều props, có thể vấn đề nằm ở việc component đang làm quá nhiều việc.

7. Tạo transition có thể tái sử dụng

Vue cung cấp <Transition><TransitionGroup> để xử lý animation theo trạng thái.

<Transition name="fade">
  <div v-if="visible">
    Nội dung
  </div>
</Transition>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
 
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

Bạn nên chuẩn hóa các transition phổ biến:

  • Fade.
  • Slide.
  • Scale.
  • Collapse.
  • Dialog.
  • Dropdown.
  • Page transition.

Không nên viết lại cùng một nhóm CSS animation trong hàng chục component.

Tuy nhiên, animation cần phục vụ trải nghiệm người dùng. Không nên thêm transition vào mọi thứ chỉ vì có thể làm được.

Hãy cân nhắc:

  • Tốc độ.
  • Khả năng nhận biết trạng thái.
  • Reduced motion.
  • Hiệu suất trên thiết bị yếu.
  • Việc animation có làm chậm thao tác hay không.

8. Chọn HTTP client theo nhu cầu thực tế

Axios vẫn là một thư viện tốt, nhưng không phải mọi dự án đều bắt buộc phải dùng Axios.

Trong trình duyệt hiện đại, fetch đã đủ cho nhiều trường hợp:

const response = await fetch('/api/users')
 
if (!response.ok) {
  throw new Error('Failed to fetch users')
}
 
const users = await response.json()

Bạn nên dùng fetch khi:

  • API đơn giản.
  • Không cần interceptor phức tạp.
  • Không cần nhiều tính năng đặc thù.
  • Muốn giảm dependency.

Axios có thể phù hợp khi cần:

  • Interceptor.
  • Tự động xử lý request/response.
  • Timeout thuận tiện.
  • Cấu hình client dùng chung.
  • Hệ thống API lớn.
  • Tương thích với kiến trúc hiện tại.

Điều quan trọng là không tạo HTTP request trực tiếp rải rác trong mọi component.

Hãy tách logic gọi API thành composable hoặc service khi ứng dụng bắt đầu phức tạp.

9. Sử dụng Vue Router đúng vai trò

Vue Router là router chính thức dành cho Vue.

Router nên chịu trách nhiệm cho:

  • URL.
  • Điều hướng.
  • Route params.
  • Query params.
  • Nested routes.
  • Navigation guards.
  • Lazy loading route.
  • Metadata của route.

Ví dụ lazy loading:

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

Không nên dùng Pinia để thay thế hoàn toàn router.

Nếu một bộ lọc có thể được chia sẻ qua URL, hãy cân nhắc lưu nó trong query:

/products?category=books&sort=price

Điều này giúp:

  • Reload trang vẫn giữ trạng thái.
  • Chia sẻ URL.
  • Browser back/forward hoạt động đúng.
  • Deep linking dễ dàng.
  • SEO hoặc SSR thuận tiện hơn khi cần.

10. Không còn sử dụng Vue 2 filters

Trong Vue 2, filters thường được viết như sau:

{{ message | capitalize }}

Vue 3 đã loại bỏ filters.

Thay vào đó, hãy dùng:

  • Computed.
  • Function.
  • Composable.
  • Utility function.
function capitalize(value: string) {
  return value.charAt(0).toUpperCase() + value.slice(1)
}

Trong template:

{{ capitalize(message) }}

Nếu giá trị được sử dụng nhiều lần:

const formattedMessage = computed(() => capitalize(message.value))

Không nên nhồi quá nhiều logic phức tạp trực tiếp vào template. Template nên mô tả giao diện, không nên trở thành nơi xử lý nghiệp vụ.

11. Đọc warning và error thay vì bỏ qua

Vue cung cấp warning khá hữu ích, đặc biệt trong môi trường development.

Một số lỗi thường gặp:

  • Mutate prop trực tiếp.
  • Key không ổn định.
  • Component không được resolve.
  • Hydration mismatch.
  • Reactive dependency không đúng.
  • Event handler không tồn tại.
  • Template có lỗi.
  • Dùng API không tương thích.

Đừng chỉ tắt warning để console “sạch”.

Một warning bị bỏ qua hôm nay có thể trở thành bug khó truy tìm trong production.

Bạn nên:

  • Đọc toàn bộ thông báo.
  • Tìm nguyên nhân gốc.
  • Kiểm tra stack trace.
  • Tạo reproduction nhỏ nếu cần.
  • Sửa kiến trúc thay vì thêm workaround.

Vue Devtools cũng rất hữu ích để kiểm tra component tree, state, events và hiệu năng.

12. Chỉ dùng JSX khi nó thực sự phù hợp

Vue hỗ trợ JSX/TSX, nhưng không có nghĩa là mọi component nên viết bằng JSX.

JSX có thể hữu ích khi:

  • Render logic rất động.
  • Tạo component framework.
  • Viết render function.
  • Xây dựng thư viện UI.
  • Cần kiểm soát cấu trúc render bằng JavaScript.
  • Component có nhiều nhánh render phức tạp.

Tuy nhiên, Single File Component với template thường dễ đọc hơn đối với giao diện thông thường.

Hãy chọn công cụ phù hợp với loại component thay vì biến mọi giao diện thành JSX.

13. Dùng CSS cho hover thay vì tạo reactive state không cần thiết

Một lỗi phổ biến là dùng JavaScript để theo dõi hover:

const isHovered = ref(false)
<div
  @mouseenter="isHovered = true"
  @mouseleave="isHovered = false"
>
  ...
</div>

Nếu mục đích chỉ là thay đổi giao diện khi hover, CSS đã đủ:

.card:hover {
  transform: translateY(-2px);
}

Chỉ dùng reactive state khi hover thực sự ảnh hưởng đến logic:

  • Hiển thị dữ liệu bổ sung.
  • Gọi API.
  • Kích hoạt tooltip phức tạp.
  • Thay đổi hành vi.
  • Theo dõi trạng thái tương tác vì mục đích accessibility hoặc analytics.

Nguyên tắc:

Đừng dùng JavaScript để giải quyết vấn đề mà CSS đã giải quyết tốt.

14. Sử dụng defineModel() cho component có v-model

Trong Vue 3.4+, defineModel() giúp đơn giản hóa việc xây dựng component hỗ trợ v-model.

Cách truyền thống yêu cầu khai báo prop và emit:

const props = defineProps<{
  modelValue: string
}>()
 
const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

Với defineModel():

<script setup lang="ts">
const model = defineModel<string>()
</script>
 
<template>
  <input v-model="model" />
</template>

Component cha:

<MyInput v-model="username" />

Cách này làm giảm boilerplate và giúp component form dễ đọc hơn.

Bạn vẫn cần hiểu:

  • v-model là sự kết hợp giữa prop và event.
  • Component con không nên tự ý thay đổi dữ liệu ngoài contract.
  • Cần xử lý kiểu dữ liệu và giá trị mặc định rõ ràng.
  • Không nên lạm dụng nhiều model nếu API component trở nên khó hiểu.

15. Hiểu đúng về this trong JavaScript

Trong Composition API với <script setup>, bạn thường không cần dùng this.

Tuy nhiên, vấn đề về this vẫn tồn tại trong JavaScript, đặc biệt khi làm việc với:

  • Callback.
  • Event handler.
  • Method được truyền làm reference.
  • Class.
  • Options API.
  • Thư viện bên ngoài.

Ví dụ:

const user = {
  name: 'Tuan',
 
  greet() {
    console.log(this.name)
  },
}
 
const greet = user.greet
greet()

Ở đây, this không còn trỏ tới user.

Nếu cần giữ context:

const greet = user.greet.bind(user)

Trong Vue 3, việc ưu tiên Composition API giúp giảm nhiều lỗi liên quan đến this, nhưng không thay thế việc hiểu JavaScript nền tảng.

16. Đừng phụ thuộc quá mức vào CSS framework

Tailwind CSS, Bootstrap, UnoCSS hoặc các hệ thống UI khác có thể giúp tăng tốc phát triển. Nhưng việc sử dụng framework không thay thế cho việc thiết kế hệ thống giao diện.

Một dự án tốt nên có:

  • Design tokens.
  • Màu sắc nhất quán.
  • Typography scale.
  • Spacing scale.
  • Border radius.
  • Shadow.
  • Component states.
  • Responsive rules.
  • Dark mode nếu cần.
  • Accessibility guidelines.

Nếu mỗi component tự chọn một màu, một khoảng cách và một kiểu button khác nhau, dự án sẽ nhanh chóng trở nên thiếu nhất quán.

CSS framework là công cụ. Design system mới là nền tảng.

17. Tránh nested watchers quá sâu

Watchers hữu ích khi cần phản ứng với thay đổi và thực hiện side effect:

watch(searchQuery, () => {
  fetchResults()
})

Nhưng nhiều watcher lồng nhau thường là dấu hiệu thiết kế state chưa tốt.

Các vấn đề có thể xảy ra:

  • Khó theo dõi luồng dữ liệu.
  • Dễ tạo vòng lặp.
  • Khó cleanup.
  • Khó kiểm thử.
  • Side effect xảy ra ngoài dự đoán.
  • Hiệu suất kém.

Hãy ưu tiên:

  • computed cho giá trị dẫn xuất.
  • Một watcher duy nhất cho một side effect.
  • Composable để gom logic.
  • Explicit function khi cần điều phối nhiều bước.
  • watchEffect khi dependency tự nhiên và đơn giản.

Nếu một giá trị chỉ phụ thuộc vào các state khác, hãy dùng computed thay vì watcher.

18. Luôn xử lý loading, error và empty state

Một giao diện gọi API không chỉ có hai trạng thái “có dữ liệu” và “không có dữ liệu”.

Thông thường cần ít nhất:

  • Initial state.
  • Loading state.
  • Success state.
  • Empty state.
  • Error state.
  • Retry state.
  • Partial state nếu API trả về một phần dữ liệu.
<template>
  <div v-if="loading">
    Loading...
  </div>
 
  <div v-else-if="error">
    Không thể tải dữ liệu.
    <button @click="reload">Thử lại</button>
  </div>
 
  <div v-else-if="items.length === 0">
    Chưa có dữ liệu.
  </div>
 
  <ItemList v-else :items="items" />
</template>

Việc mô hình hóa trạng thái rõ ràng giúp giảm bug và làm cho component dễ kiểm thử hơn.

19. Thiết kế props nhỏ, rõ ràng và có kiểu dữ liệu

Props là contract từ component cha xuống component con.

Ví dụ tốt:

defineProps<{
  title: string
  description?: string
  disabled?: boolean
}>()

Một object config khổng lồ thường khiến component khó sử dụng và khó biết thuộc tính nào thực sự cần thiết.

Thay vì:

<UserCard :config="userCardConfig" />

Có thể rõ ràng hơn:

<UserCard
  :name="user.name"
  :avatar="user.avatar"
  :role="user.role"
  :disabled="user.isDisabled"
/>

Tuy nhiên, không nên cực đoan biến mọi thứ thành hàng chục props. Nếu một nhóm dữ liệu thực sự có ý nghĩa như một object domain, việc truyền object vẫn hợp lý.

Mục tiêu là contract rõ ràng, không phải số lượng props thấp nhất.

20. Biết khi nào dùng computed và khi nào dùng watch

Dùng computed khi

Bạn cần tính toán một giá trị từ state khác:

const total = computed(() =>
  cart.value.reduce((sum, item) => sum + item.price, 0),
)

Dùng watch khi

Bạn cần thực hiện side effect khi dữ liệu thay đổi:

watch(route, () => {
  document.title = route.meta.title
})

Side effect có thể bao gồm:

  • Gọi API.
  • Ghi localStorage.
  • Cập nhật document title.
  • Gửi analytics.
  • Đồng bộ với thư viện bên ngoài.
  • Thực hiện thao tác bất đồng bộ.

Không nên dùng watcher để tạo ra dữ liệu mà computed có thể tính toán.

Sai:

const total = ref(0)
 
watch(items, () => {
  total.value = items.value.reduce(...)
})

Tốt hơn:

const total = computed(() =>
  items.value.reduce(...),
)

Quy tắc:

computed tạo ra giá trị; watch thực hiện tác động phụ.

21. Hiểu các thay đổi và bẫy phổ biến của Vue 3

Một số thói quen từ Vue 2 không còn đúng trong Vue 3:

  • Filters đã bị loại bỏ.
  • Vuex không còn là lựa chọn mặc định cho dự án mới.
  • beforeDestroydestroyed được thay bằng beforeUnmountunmounted.
  • Một component có thể có nhiều root node.
  • Composition API thay đổi cách tổ chức logic.
  • ref cần sử dụng .value trong JavaScript.
  • Một số API global đã thay đổi.
  • Plugin phải được đăng ký trên app instance.
  • Một số behavior liên quan đến attribute fallthrough thay đổi.
  • Cơ chế reactivity dựa trên Proxy khác với Vue 2.

Khi nâng cấp từ Vue 2 lên Vue 3, không nên chỉ sửa lỗi compile. Bạn cần kiểm tra cả:

  • State management.
  • Router.
  • Form handling.
  • Lifecycle.
  • Plugin.
  • Test.
  • Build tool.
  • SSR hoặc hydration.
  • Third-party component libraries.

22. Hiểu rõ khác biệt giữa ref, reactive và destructuring

Đây là nguồn gây lỗi phổ biến trong Composition API.

ref

const count = ref(0)
 
count.value++

reactive

const state = reactive({
  count: 0,
})
 
state.count++

Một lỗi thường gặp:

const state = reactive({
  count: 0,
})
 
const { count } = state

Việc destructure như trên có thể làm mất liên kết reactive trong nhiều trường hợp.

Nếu cần destructure reactive object mà vẫn giữ reactivity:

const { count } = toRefs(state)

Hoặc sử dụng storeToRefs() khi làm việc với Pinia store.

Đừng coi refreactive là hai cách viết hoàn toàn tương đương. Hãy chọn dựa trên cách bạn muốn truyền, destructure và tái sử dụng dữ liệu.

23. Hiểu đúng lifecycle và vấn đề SSR

onMounted() chỉ chạy sau khi component được mount trên client.

Đây là nơi phù hợp để truy cập:

  • window.
  • document.
  • DOM.
  • Browser API.
  • Local storage.
  • Một số thư viện chỉ chạy trên client.

Nhưng nếu ứng dụng sử dụng SSR, bạn không nên giả định mọi code đều chạy trong browser.

Ví dụ không an toàn:

const theme = localStorage.getItem('theme')

Nếu code được thực thi trong môi trường server, localStorage không tồn tại.

Cần phân biệt:

  • Code chạy trên server.
  • Code chạy trên client.
  • Code chạy trong quá trình hydration.
  • Code chạy sau khi component mount.

Ngoài ra, khi dùng event listener hoặc subscription, hãy cleanup:

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

Lifecycle hooks không chỉ để chạy code đúng thời điểm, mà còn để quản lý vòng đời tài nguyên.

24. Ưu tiên events, slots và composables thay vì truyền function props quá mức

Truyền function qua props đôi khi hữu ích:

<DataTable :formatter="formatValue" />

Nhưng nếu component bắt đầu nhận quá nhiều callback:

<ComplexComponent
  :on-save="handleSave"
  :on-delete="handleDelete"
  :on-hover="handleHover"
  :on-format="formatValue"
  :on-validate="validate"
/>

thì API của component có thể trở nên khó hiểu.

Trong nhiều trường hợp, bạn nên cân nhắc:

  • Emit events.
  • Slots.
  • Scoped slots.
  • Composables.
  • Dependency injection.
  • Tách component nhỏ hơn.

Ví dụ:

<DataTable
  @save="handleSave"
  @delete="handleDelete"
/>

Slots cũng rất phù hợp khi component cần cho phép tùy biến phần hiển thị:

<DataTable>
  <template #empty>
    Chưa có dữ liệu.
  </template>
</DataTable>

Function props không bị cấm, nhưng nên được sử dụng khi nó thực sự làm API rõ ràng hơn.

25. Không mutate props trực tiếp

Props là dữ liệu đi từ component cha xuống component con. Component con không nên trực tiếp thay đổi props.

Không nên:

const props = defineProps<{
  count: number
}>()
 
props.count++

Thay vào đó, hãy:

  • Emit event lên component cha.
  • Dùng v-model.
  • Tạo local state nếu cần chỉnh sửa độc lập.
  • Dùng computed nếu chỉ cần biến đổi giá trị để hiển thị.

Luồng dữ liệu nên rõ ràng:

Parent state

Props

Child component

Emit event

Parent cập nhật state

Luồng dữ liệu một chiều giúp ứng dụng dễ debug và dễ bảo trì hơn.

26. Sử dụng dynamic classes thay vì tạo quá nhiều component trạng thái

Nhiều giao diện có các trạng thái như:

  • Active.
  • Disabled.
  • Selected.
  • Loading.
  • Error.
  • Success.
  • Expanded.
  • Collapsed.

Không phải trạng thái nào cũng cần một component riêng.

Bạn có thể dùng dynamic class:

<button
  :class="[
    'button',
    {
      'button--active': active,
      'button--disabled': disabled,
      'button--loading': loading,
    },
  ]"
>
  {{ label }}
</button>

Hoặc object syntax:

<div
  :class="{
    active: isActive,
    disabled: isDisabled,
  }"
>
  Nội dung
</div>

Khi class logic trở nên phức tạp, hãy tách thành computed:

const buttonClasses = computed(() => ({
  button: true,
  'button--active': active.value,
  'button--disabled': disabled.value,
  'button--loading': loading.value,
}))

Dynamic classes giúp giữ component linh hoạt mà không cần tạo ra quá nhiều biến thể component chỉ khác nhau ở một vài trạng thái giao diện.

Những điều quan trọng hơn cả 26 mẹo

Các mẹo trên giúp tiết kiệm thời gian ở cấp độ component. Nhưng để phát triển Vue hiệu quả trong năm 2026, bạn còn cần chú ý đến những vấn đề lớn hơn.

1. Bắt đầu dự án bằng công cụ hiện đại

Với ứng dụng Vue mới, Vite thường là lựa chọn mặc định phù hợp.

Bạn nên cân nhắc:

  • Vue 3.
  • Vite.
  • TypeScript.
  • Vue Official extension.
  • vue-tsc.
  • Vitest.
  • Pinia nếu cần global state.
  • Vue Router nếu ứng dụng có nhiều route.
  • ESLint hoặc Biome tùy quy ước dự án.
  • Vue Devtools.

Vue CLI hiện ở trạng thái Maintenance Mode; các dự án mới nên sử dụng công cụ dựa trên Vite và create-vue.

2. Chọn framework phù hợp với loại ứng dụng

Không phải ứng dụng nào cũng chỉ cần Vue + Vite.

Bạn có thể cân nhắc:

  • Vue + Vite cho SPA hoặc frontend độc lập.
  • Nuxt cho SSR, routing tích hợp, server routes và full-stack Vue.
  • VitePress cho tài liệu và documentation.
  • Astro khi nội dung là trọng tâm và Vue chỉ được dùng cho các interactive island.
  • Các framework khác nếu dự án có yêu cầu đặc biệt.

Không nên chọn framework chỉ vì nó phổ biến. Hãy chọn dựa trên:

  • Có cần SSR không?
  • Có cần SEO không?
  • Có cần server routes không?
  • Có cần content-driven website không?
  • Có cần SPA thuần không?
  • Có cần triển khai đơn giản không?
  • Có cần tối ưu JavaScript gửi xuống trình duyệt không?

3. TypeScript không chỉ để tránh lỗi kiểu dữ liệu

TypeScript giúp:

  • Props rõ ràng.
  • Emits có contract.
  • API response có kiểu.
  • Store dễ sử dụng.
  • Refactor an toàn hơn.
  • IDE autocomplete tốt hơn.
  • Giảm lỗi khi đổi tên hoặc thay đổi cấu trúc dữ liệu.

Tuy nhiên, TypeScript chỉ hữu ích nếu bạn không liên tục né tránh nó bằng:

any

hoặc:

as any

Một codebase Vue tốt nên cố gắng mô hình hóa domain rõ ràng thay vì chỉ thêm type để làm trình biên dịch im lặng.

4. Viết test cho logic quan trọng

Không phải mọi component đều cần test chi tiết từng dòng. Nhưng những logic quan trọng nên được kiểm thử:

  • Validation.
  • Tính toán giá trị.
  • Composables.
  • Store.
  • Permission.
  • Form submission.
  • Các trạng thái loading/error.
  • Những bug từng xảy ra.
  • Component quan trọng trong sản phẩm.

Vitest thường phù hợp với hệ sinh thái Vite và các dự án Vue hiện đại.

5. Đừng tối ưu hiệu suất trước khi đo lường

Không phải cứ dùng:

  • shallowRef.
  • markRaw.
  • v-memo.
  • Lazy loading.
  • Virtual list.
  • Dynamic import.
  • Web Worker.

là ứng dụng sẽ tốt hơn.

Trước tiên hãy xác định vấn đề:

  • Bundle quá lớn?
  • Render quá nhiều?
  • Watcher chạy quá thường xuyên?
  • API chậm?
  • DOM quá lớn?
  • Component mount quá lâu?
  • Hydration chậm?
  • Network request bị lặp?
  • Memory leak?

Tối ưu hiệu suất nên dựa trên đo lường, không phải dựa trên cảm giác.

Bảng chuyển đổi tư duy từ Vue cũ sang Vue hiện đại

Tư duy cũCách tiếp cận phù hợp hơn trong Vue 2026
Vuex cho mọi stateComposables hoặc Pinia tùy phạm vi state
Ép component render lạiKiểm tra reactivity và thiết kế state
Một root node bắt buộcCó thể dùng nhiều root node
FiltersComputed hoặc utility functions
Tự viết validation mọi nơiSchema validation hoặc form library khi cần
Dùng Axios mặc địnhChọn fetch hoặc Axios theo nhu cầu
Dùng watcher cho mọi thứComputed cho derived state, watch cho side effect
Truyền nhiều callback propsEvents, slots và composables
Mutate propsEmit event hoặc v-model
Tự quản lý modelValue dài dòngdefineModel() trong Vue 3.4+
Dùng JavaScript cho hoverCSS cho tương tác trình bày đơn giản
Component làm mọi việcComponent contract rõ ràng và tách trách nhiệm
Chỉ quan tâm success stateLoading, error, empty và retry state
Dùng framework theo thói quenChọn Vite, Nuxt, VitePress hoặc Astro theo mục tiêu
Chỉ kiểm tra bằng mắtDevtools, type checking và automated tests

Kết luận

Tiết kiệm thời gian khi phát triển Vue không chỉ là biết thêm vài thủ thuật cú pháp. Quan trọng hơn là biết khi nào không nên viết thêm code.

Một developer Vue hiệu quả trong năm 2026 nên:

  • Hiểu rõ reactivity.
  • Phân biệt local state, shared state và URL state.
  • Dùng computed cho dữ liệu dẫn xuất.
  • Dùng watch cho side effect.
  • Không mutate props.
  • Không ép render lại khi chưa hiểu nguyên nhân.
  • Thiết kế component contract rõ ràng.
  • Tách logic thành composables khi cần.
  • Sử dụng Pinia có chọn lọc.
  • Xử lý đầy đủ loading, error và empty state.
  • Dùng TypeScript để mô hình hóa dữ liệu.
  • Dùng Vite và các công cụ hiện đại.
  • Viết test cho logic quan trọng.
  • Đo lường trước khi tối ưu hiệu suất.
  • Không biến framework thành nơi chứa mọi loại logic.

Vue giúp bạn xây dựng giao diện nhanh. Nhưng kiến trúc tốt mới giúp bạn không phải xây lại chúng.