Pinia 3 và TanStack Query 5 cho Vue 3.6 - State Management Hiện đại trong kỷ nguyên Vapor Mode 2026

Posted on: 4/17/2026 12:08:59 AM

Table of contents

  1. 1. Vì sao state management cho Vue lại đổi thay năm 2026
    1. Câu hỏi đặt nền cho cả bài viết
  2. 2. Hành trình tiến hoá - từ Vuex 3 tới Pinia 3 và TanStack Query 5
  3. 3. Phân loại state - bản đồ quyết định trước khi viết store
    1. 3.1. Server state - dữ liệu thuộc về backend
    2. 3.2. UI state - trạng thái phiên làm việc
    3. 3.3. Form state - trạng thái nhập liệu
    4. 3.4. URL state - trạng thái trong query string
      1. Cảnh báo: nhồi server state vào Pinia là một dạng tech debt im lặng
  4. 4. Pinia 3 - kiến trúc store và các pattern thực dụng
    1. 4.1. Hai cú pháp - Options và Setup
    2. 4.2. Composing store - cách Pinia thay thế module lồng
    3. 4.3. Plugin system - mở rộng mọi store một cách lành mạnh
    4. 4.4. SSR và Pinia với Nuxt 4
  5. 5. TanStack Query 5 - quản lý server state
    1. 5.1. Bốn khái niệm gốc
    2. 5.2. Cấu hình QueryClient cho production
    3. 5.3. Optimistic update - kiểu mutation không có cảm giác chờ
  6. 6. Phối hợp Pinia và TanStack Query - phân vai rõ ràng
  7. 7. Vapor Mode - tận dụng compile-time reactivity cho store
    1. Mẹo khi mix Vapor và non-Vapor trong cùng app
  8. 8. Testing - kiểm thử store và query một cách có kỷ luật
    1. 8.1. Test Pinia store
    2. 8.2. Test TanStack Query
  9. 9. Migration từ Vuex sang Pinia - đường tắt thực tế
    1. Đặc biệt với Nuxt 2 và Nuxt 3 cũ
  10. 10. Hiệu năng - các đường rò thường gặp và cách bịt
    1. 10.1. Computed phụ thuộc tham chiếu mảng/đối tượng lớn
    2. 10.2. Mutation không invalidate đúng query key
    3. 10.3. Không dùng placeholderData cho phân trang
    4. 10.4. Bật devtools trong production
  11. 11. SSR và Hydration - những cạm bẫy quen mặt
  12. 12. Checklist trước khi merge một PR đụng tới state
  13. 13. Kết luận - state management Vue đã trưởng thành theo cách rất Vue
    1. Bước tiếp theo cho team đang chuyển đổi
    2. Nguồn tham khảo

1. Vì sao state management cho Vue lại đổi thay năm 2026

Trong vòng tám năm kể từ khi Vuex 3 ra đời, mô hình quản lý state cho Vue đã đi qua ba thế hệ. Vuex 3 chia state thành các module tĩnh, dùng mutations đồng bộ và actions bất đồng bộ. Vuex 4 thử cách hoà giải với Composition API nhưng vẫn vướng kiểu boilerplate cũ. Pinia ra mắt như một thư viện thử nghiệm năm 2019 và dần được công nhận là store chính thức của Vue, kế thừa toàn bộ vai trò của Vuex từ Vue 3.4 trở đi. Năm 2026, khi Vue 3.6 mở khoá Vapor Mode với cơ chế reactivity biên dịch ra mã tinh gọn không cần Virtual DOM, mô hình state lại một lần nữa cần điều chỉnh để khai thác hiệu năng mới.

Đồng thời, cộng đồng frontend đã chấp nhận một sự phân chia rõ rệt mà 5 năm trước chưa hề tồn tại: server state (dữ liệu thuộc về backend, có vòng đời cache, refresh, mutation) và UI/client state (trạng thái thuộc về phiên làm việc trên trình duyệt, không cần bền vững). Vue 3.6 năm 2026 không cố nhồi cả hai loại vào một thư viện duy nhất. Pinia 3 được tinh chỉnh để chuyên trị UI state, còn TanStack Query 5 (trước đây là Vue Query) trở thành lựa chọn mặc định cho server state. Bài viết này phân tích cách hai thư viện phối hợp, các kiểu kiến trúc store hiện đại, cách tận dụng Vapor Mode, và checklist khi mang chúng vào sản xuất.

3.0Pinia phiên bản 2026 với HMR plugin và devtools mới
5.xTanStack Query với hỗ trợ Vapor Mode và Suspense API
3.6Vue 3.6 mở khoá Vapor Mode opt-in cho component và store
~40%Bundle giảm khi gỡ Vuex và bật Vapor cho store nhẹ

Câu hỏi đặt nền cho cả bài viết

Khi một component Vue cần đọc dữ liệu, có ba câu hỏi nền tảng phải trả lời trước khi viết dòng code đầu tiên: dữ liệu này có phải là sự thật của server không? Có cần chia sẻ giữa nhiều component không? Vòng đời của nó kết thúc khi component bị unmount hay theo phiên người dùng? Trả lời được ba câu hỏi này sẽ tự khắc chọn được Pinia, TanStack Query, hay đơn thuần là ref trong setup script.

2. Hành trình tiến hoá - từ Vuex 3 tới Pinia 3 và TanStack Query 5

Hiểu được trình tự các bước này giúp lý giải vì sao cộng đồng Vue chia hai loại state, vì sao Pinia thắng Vuex, và vì sao TanStack Query không bị xem là cạnh tranh với Pinia.

2017 - Vuex 2 và mô hình Flux
Vuex 2 mượn ý tưởng Redux/Flux: state là một cây bất biến, thay đổi qua mutations đồng bộ và actions bất đồng bộ. Đẹp về mặt lý thuyết nhưng tạo ra khá nhiều file, và mọi thứ đều phải khai báo trước.
2019 - Composition API và Pinia thử nghiệm
Eduardo San Martin Morote viết Pinia như một bản RFC để đề xuất store nhẹ hơn cho Vue 3. Mỗi store là một setup function như component, dùng ref, computed, watch trực tiếp. Không còn mutations, không còn module lồng nhau cứng nhắc.
2021 - Vue Query (Tanner Linsley + Damian Osipiuk)
React Query đã chứng minh server state cần một thư viện riêng với cache, refetch, deduplication, retry, mutation, optimistic update. Phiên bản Vue Query ra đời, sau đó nhập vào TanStack Query để dùng chung core với React, Solid, Svelte.
2023 - Pinia thay thế Vuex chính thức
Vue Core team công bố Pinia là store chính thức cho Vue, kèm khuyến nghị migration. Vuex 4 không thêm tính năng mới, chỉ duy trì cho dự án legacy. Pinia có devtools tích hợp, time-travel, plugin system đầy đủ.
2024 - TanStack Query 5 và Suspense first-class
TanStack Query 5 viết lại core, hỗ trợ useSuspenseQuery, infinite query với cursor và offset, và mutation observer mới. Vue adapter dùng đúng reactivity nguyên thuỷ, không tạo proxy phụ.
2026 - Vue 3.6 Vapor và Pinia 3
Pinia 3 tương thích cả Vue 3 thường và Vapor Mode. Bộ devtools mới hỗ trợ tracing reactivity nguyên thuỷ. TanStack Query bổ sung Vapor-aware adapter giúp giảm overhead cho component không dùng Virtual DOM.

3. Phân loại state - bản đồ quyết định trước khi viết store

Sai lầm phổ biến nhất với người mới là gom hết mọi thứ vào một store khổng lồ. Năm 2026, cách nhìn được cộng đồng đồng thuận là chia state thành bốn nhóm rõ ràng và chọn công cụ phù hợp cho từng nhóm.

flowchart TD
    A[Component cần dữ liệu] --> B{Server là nguồn sự thật?}
    B -- Có --> C[TanStack Query]
    B -- Không --> D{Chia sẻ giữa nhiều component?}
    D -- Không --> E[ref/reactive trong setup]
    D -- Có --> F{Theo phiên người dùng?}
    F -- Có --> G[Pinia store]
    F -- Không --> H[provide/inject scoped]
    G --> I{Cần đồng bộ giữa tab?}
    I -- Có --> J[Pinia + BroadcastChannel plugin]
    I -- Không --> K[Pinia thuần]

Hình 1: Cây quyết định chọn công cụ state cho Vue 3.6 năm 2026

3.1. Server state - dữ liệu thuộc về backend

Đặc trưng của loại này: có một nguồn sự thật ngoài client (REST/GraphQL/RPC), cần cache để tránh request trùng, cần refetch khi điều kiện thay đổi, cần invalidate sau mutation. Đây là vùng đất của TanStack Query, không phải Pinia. Cố nhồi server state vào Pinia thường dẫn tới việc tự viết lại các tính năng đã có sẵn: dedupe, retry, polling, optimistic update, garbage collection, focus refetch.

3.2. UI state - trạng thái phiên làm việc

Theme tối/sáng, ngôn ngữ hiển thị, sidebar đang mở hay đóng, filter của bảng dữ liệu, lựa chọn tab hiện tại. Loại này thuộc về Pinia. Có thể bền vững xuyên trang nhờ plugin pinia-plugin-persistedstate ghi xuống localStorage hoặc sessionStorage.

3.3. Form state - trạng thái nhập liệu

Form không nên đi vào Pinia, càng không nên đi vào TanStack Query. Dùng VeeValidate 4 hoặc FormKit 1 để giữ values, errors, touched, dirty. Khi submit thì gọi mutate của TanStack Query để gửi server và invalidateQueries để refresh phần liên quan.

3.4. URL state - trạng thái trong query string

Filter, phân trang, từ khoá tìm kiếm là URL state. Đặt vào route.query của Vue Router 5 để có thể bookmark, chia sẻ link, back/forward không mất ngữ cảnh. TanStack Query đọc trực tiếp từ computed bao quanh route để key cache đúng.

Cảnh báo: nhồi server state vào Pinia là một dạng tech debt im lặng

Khi bạn viết một Pinia store có state.users, actions.fetchUsers, actions.createUser, actions.updateUser, bạn đang viết lại TanStack Query bằng tay. Vài tháng sau sẽ phải thêm tracking isLoading, isError, cache TTL, dedupe khi 5 component cùng gọi. Mỗi tính năng tự thêm là một chỗ rò bug tiềm tàng. Đặt server state đúng chỗ ngay từ đầu.

4. Pinia 3 - kiến trúc store và các pattern thực dụng

4.1. Hai cú pháp - Options và Setup

Pinia hỗ trợ hai cách khai báo store. Options style giống Vue 2 cho người quen Vuex. Setup style giống Composition API và là khuyến nghị mặc định năm 2026 vì gọn, tuỳ biến cao, dễ test.

// stores/auth.js - Setup style (khuyến nghị)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(null)

  const isAuthenticated = computed(() => !!token.value)
  const initials = computed(() => {
    if (!user.value?.name) return ''
    return user.value.name.split(' ').map(p => p[0]).join('').toUpperCase()
  })

  function setSession(payload) {
    user.value = payload.user
    token.value = payload.token
  }

  function logout() {
    user.value = null
    token.value = null
  }

  return { user, token, isAuthenticated, initials, setSession, logout }
})

4.2. Composing store - cách Pinia thay thế module lồng

Vuex có module lồng để tổ chức state lớn. Pinia bỏ hoàn toàn ý tưởng đó - thay vào đó, một store có thể use một store khác trong nội tại. Tổ chức ngang theo domain thay vì lồng theo cây.

// stores/cart.js
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'

export const useCartStore = defineStore('cart', () => {
  const auth = useAuthStore()
  const items = ref([])

  const subtotal = computed(() =>
    items.value.reduce((s, i) => s + i.price * i.qty, 0)
  )
  const tax = computed(() =>
    auth.user?.country === 'VN' ? subtotal.value * 0.08 : 0
  )
  const total = computed(() => subtotal.value + tax.value)

  return { items, subtotal, tax, total }
})

4.3. Plugin system - mở rộng mọi store một cách lành mạnh

Plugin Pinia là một function nhận { store } và có thể bổ sung property, theo dõi mutation, can thiệp action. Ba plugin được dùng phổ biến năm 2026:

  • pinia-plugin-persistedstate: tự ghi state xuống localStorage/sessionStorage/cookies, có config riêng từng store.
  • pinia-undo: thêm store.undo()store.redo() cho các store cần history.
  • pinia-shared-state: dùng BroadcastChannel API để đồng bộ state giữa nhiều tab cùng origin (rất hữu ích cho auth, theme).
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

// stores/preferences.js
export const usePreferences = defineStore('prefs', () => {
  const theme = ref('light')
  const locale = ref('vi')
  return { theme, locale }
}, {
  persist: { storage: localStorage, paths: ['theme', 'locale'] }
})

4.4. SSR và Pinia với Nuxt 4

Nuxt 4 tự khởi tạo Pinia mỗi request và serialize state vào HTML payload. Tránh truy cập window, document, hay localStorage trong setup function của store - chúng không tồn tại ở phía server. Thay vào đó, dùng onMounted ở component hoặc bọc trong if (process.client).

5. TanStack Query 5 - quản lý server state

5.1. Bốn khái niệm gốc

Không cần biết hết hàng trăm option, chỉ cần nắm vững bốn khái niệm: query key (định danh cache), query function (cách lấy dữ liệu), mutation (thay đổi server), invalidation (đánh dấu cache cần refetch). Mọi thứ khác chỉ là biến thể.

// composables/useUsers.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'

export function useUsers(filter) {
  return useQuery({
    queryKey: ['users', filter],
    queryFn: ({ signal }) => fetch(`/api/users?role=${filter.value}`, { signal })
      .then(r => r.json()),
    staleTime: 60_000,
    placeholderData: (prev) => prev
  })
}

export function useCreateUser() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: (payload) => fetch('/api/users', {
      method: 'POST', body: JSON.stringify(payload)
    }).then(r => r.json()),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] })
  })
}

5.2. Cấu hình QueryClient cho production

Default của TanStack Query rất bảo thủ: refetch on window focus, retry 3 lần, không có staleTime. Production thường cần điều chỉnh.

import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000,        // dữ liệu coi là tươi trong 30s
      gcTime: 5 * 60_000,       // dọn rác sau 5 phút không dùng
      refetchOnWindowFocus: false,
      retry: (failureCount, err) => err.status >= 500 && failureCount < 2,
      networkMode: 'offlineFirst'
    },
    mutations: {
      retry: false,
      networkMode: 'always'
    }
  }
})

app.use(VueQueryPlugin, { queryClient })

5.3. Optimistic update - kiểu mutation không có cảm giác chờ

Khi thao tác có khả năng cao thành công (like, follow, edit comment), nên cập nhật UI ngay rồi đồng bộ với server. Nếu server từ chối thì rollback. TanStack Query chuẩn hoá mô hình này qua onMutate, onError, onSettled.

const toggleFavorite = useMutation({
  mutationFn: (postId) => api.toggleFavorite(postId),
  onMutate: async (postId) => {
    await qc.cancelQueries({ queryKey: ['posts'] })
    const prev = qc.getQueryData(['posts'])
    qc.setQueryData(['posts'], (old) =>
      old.map(p => p.id === postId
        ? { ...p, favorited: !p.favorited }
        : p)
    )
    return { prev }
  },
  onError: (err, postId, context) => {
    qc.setQueryData(['posts'], context.prev)
  },
  onSettled: () => qc.invalidateQueries({ queryKey: ['posts'] })
})

6. Phối hợp Pinia và TanStack Query - phân vai rõ ràng

Đây là phần thực hành quyết định chất lượng kiến trúc. Quy tắc đơn giản: Pinia giữ thông tin về tôi là ai và tôi đang làm gì (auth, theme, filter), TanStack Query giữ dữ liệu mà server biết.

flowchart LR
    U[Component] --> P[Pinia: auth, prefs, filter]
    U --> Q[TanStack Query: users, posts, orders]
    P --> F[filter ref]
    F --> QK[queryKey]
    QK --> Q
    Q --> API[REST/GraphQL]
    M[Mutation] --> API
    M --> INV[invalidateQueries]
    INV --> Q

Hình 2: Pinia làm nguồn cho query key, TanStack Query làm nguồn cho dữ liệu thực sự

// composables/useFilteredOrders.js
import { computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'
import { useOrderFilter } from '@/stores/order-filter'

export function useFilteredOrders() {
  const filter = useOrderFilter()  // Pinia store giữ filter UI

  const queryKey = computed(() => ['orders', {
    status: filter.status,
    from: filter.from,
    to: filter.to,
    page: filter.page
  }])

  return useQuery({
    queryKey,
    queryFn: () => api.getOrders(filter.toQuery()),
    placeholderData: (prev) => prev,
    staleTime: 10_000
  })
}

Khi user đổi filter trên Pinia, query key thay đổi, TanStack Query tự refetch. Nhờ placeholderData giữ dữ liệu cũ, UI không nhảy về trạng thái loading khi chuyển trang. Đây là pattern chuẩn cho các trang admin có bảng dữ liệu lớn.

7. Vapor Mode - tận dụng compile-time reactivity cho store

Vue 3.6 Vapor Mode biên dịch trực tiếp template thành mã thao tác DOM tinh gọn, không cần Virtual DOM. Pinia 3 tương thích cả Vapor và non-Vapor, nhưng có vài điểm cần lưu ý để tránh anti-pattern.

Khía cạnhVue 3 thườngVue 3.6 Vapor
Cập nhật DOMDiff Virtual DOM rồi patchUpdate trực tiếp text/attribute binding
ReactivityProxy + dep tracking lúc renderEffect được biên dịch thành mã chính xác cho từng binding
Bundle component nhỏ~12 KB runtime~5 KB runtime cho subtree Vapor
Pinia storeRef + computed bình thườngNhư cũ, không cần đổi
TanStack QueryVue adapter mặc địnhVapor adapter giảm tracking phí
DevtoolsCây component + stateCây binding + effect

Mẹo khi mix Vapor và non-Vapor trong cùng app

Vue 3.6 cho phép một component opt-in Vapor bằng directive vapor trong <script setup>. Pinia store không bị ảnh hưởng - cùng một store dùng được cho cả hai loại. Bắt đầu chuyển dần các component lá có nhiều binding đơn giản (badge, count, theme switcher) sang Vapor để hái quả nhanh.

8. Testing - kiểm thử store và query một cách có kỷ luật

8.1. Test Pinia store

Pinia cung cấp setActivePinia(createPinia()) để khởi tạo lại store cho mỗi test. Vitest là combo phổ biến nhất.

// stores/cart.spec.js
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach } from 'vitest'
import { useCartStore } from './cart'

describe('cart store', () => {
  beforeEach(() => setActivePinia(createPinia()))

  it('tính subtotal đúng cho nhiều item', () => {
    const cart = useCartStore()
    cart.items = [
      { price: 100, qty: 2 },
      { price: 50, qty: 3 }
    ]
    expect(cart.subtotal).toBe(350)
  })
})

8.2. Test TanStack Query

Tạo một QueryClient mới cho mỗi test, mock fetch hoặc dùng MSW (Mock Service Worker) để chặn ở tầng network. Đừng test cache logic - đó là phần TanStack Query đã test sẵn cho bạn.

import { mount } from '@vue/test-utils'
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'

function createWrapper(component) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } }
  })
  return mount(component, {
    global: { plugins: [[VueQueryPlugin, { queryClient }]] }
  })
}

9. Migration từ Vuex sang Pinia - đường tắt thực tế

Dự án legacy Vue 2/3 dùng Vuex còn rất nhiều. Quy trình chuyển ít rủi ro nhất gồm bốn bước, làm song song chứ không big-bang.

  1. Cài Pinia bên cạnh Vuex. Hai thư viện có thể tồn tại song song. Component cũ tiếp tục dùng useStore, component mới chuyển sang useXxxStore.
  2. Map từng module Vuex thành một Pinia store. State giữ nguyên, getters thành computed, mutations gộp thẳng vào actions, actions giữ nguyên logic. Mất khoảng 30% dòng code so với Vuex.
  3. Thay thế lời gọi component theo nhánh. Mỗi PR đổi một module, kiểm thử kỹ. Tránh đổi toàn bộ trong một lần commit.
  4. Gỡ Vuex khi không còn import nào. Dùng grep hoặc knip để xác minh, sau đó xoá khỏi package.json.

Đặc biệt với Nuxt 2 và Nuxt 3 cũ

Nuxt 2 dùng Vuex theo convention store/index.js. Nuxt 3+ chỉ hỗ trợ Pinia. Dự án Nuxt 2 cần được nâng cấp framework trước, vì cố nhồi Pinia vào Nuxt 2 sẽ vướng SSR serialization. Lộ trình hợp lý: Nuxt 2 → Nuxt Bridge → Nuxt 3.x → Nuxt 4.

10. Hiệu năng - các đường rò thường gặp và cách bịt

Pinia và TanStack Query đều đã tối ưu khá tốt, nhưng vẫn có vài bẫy hiệu năng quen thuộc nếu không cẩn thận.

10.1. Computed phụ thuộc tham chiếu mảng/đối tượng lớn

Khi computed đọc một mảng lớn rồi .map().filter() tạo mảng mới mỗi lần invalidation, nó kéo theo re-render hàng loạt component dùng nó. Tách thành nhiều computed nhỏ hơn hoặc dùng shallowRef cho mảng không cần reactivity sâu.

10.2. Mutation không invalidate đúng query key

Một lỗi rất phổ biến: onSuccess chỉ invalidateQueries({ queryKey: ['users'] }) nhưng query thật sự là ['users', { role: 'admin' }]. TanStack Query mặc định partial-match nên thường chạy đúng, nhưng khi cần chính xác hãy dùng { exact: true }.

10.3. Không dùng placeholderData cho phân trang

Khi user click trang 2, cache cũ của trang 1 vẫn còn, nhưng cache trang 2 chưa có. Mặc định UI sẽ rớt về trạng thái loading. Dùng placeholderData: (prev) => prev để giữ data cũ trong khi data mới đang fetch, tạo cảm giác mượt như native.

10.4. Bật devtools trong production

Pinia devtools và TanStack Query devtools đều mặc định disable trong production, nhưng nếu bạn import sai cách có thể vô tình kéo chúng vào bundle. Dùng dynamic import có điều kiện import.meta.env.DEV.

11. SSR và Hydration - những cạm bẫy quen mặt

Với Nuxt 4 và Vite SSR, cả Pinia lẫn TanStack Query đều có cơ chế serialize/deserialize state vào HTML payload để hydrate ở client mà không refetch. Hai điểm cần nhớ:

  • Trong Pinia, không truy cập window/document trong store setup function - dùng onMounted ở component nếu cần.
  • Trong TanStack Query, dùng dehydrate(queryClient) ở server và hydrate(queryClient, state) ở client. Nuxt 4 module @tanstack/vue-query/nuxt tự động hoá toàn bộ.
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt', '@tanstack/vue-query/nuxt'],
  vueQuery: {
    queryClientConfig: {
      defaultOptions: { queries: { staleTime: 30_000 } }
    }
  }
})

12. Checklist trước khi merge một PR đụng tới state

  • State này có phải server state không? Nếu phải, dùng TanStack Query, đừng nhồi vào Pinia.
  • Pinia store có chia sẻ giữa nhiều component không? Nếu không, đặt vào component đó qua ref/reactive.
  • Query key có phụ thuộc filter từ Pinia? Dùng computed bao quanh để tự reactive.
  • Mutation có gọi invalidateQueries đúng key sau khi thành công?
  • Optimistic update có rollback trong onError?
  • Pinia store dùng SSR không? Tránh truy cập browser API trong setup.
  • Persist state có loại trừ field nhạy cảm (token, password) khỏi localStorage?
  • Devtools đã bị tắt trong production build chưa?
  • Test có setActivePinia(createPinia())beforeEach để tránh leak state giữa các test?
  • Migration Vuex còn module nào cần chuyển không? Dùng knip tìm import còn sót.

13. Kết luận - state management Vue đã trưởng thành theo cách rất Vue

Bài học từ năm 2026 không phải là "Pinia tốt hơn Vuex" hay "TanStack Query tốt hơn Pinia". Bài học đúng là: state có nhiều loại, mỗi loại cần một công cụ phù hợp, và Vue ecosystem cuối cùng đã có bộ công cụ đó. Pinia 3 chuyên trị UI state với cú pháp gọn nhẹ, kế thừa tinh thần Composition API. TanStack Query 5 chuyên trị server state với cache, mutation, optimistic update đã tinh luyện qua hàng triệu app React. Vue Router 5 giữ URL state. VeeValidate hoặc FormKit giữ form state.

Khi bốn lớp này chia vai rõ ràng, code Vue không còn rối, không còn boilerplate kiểu Vuex, không còn cảnh tự viết lại cache cho từng resource. Vapor Mode chỉ là phần thưởng thêm về hiệu năng - kiến trúc đúng mới là chìa khoá. Năm 2026, một codebase Vue gọn gàng trông sẽ giống như: store ngắn vì server state đã được TanStack Query lo, component ngắn vì store đã thay thế tham số truyền tay, và phần lớn dòng code dành cho logic nghiệp vụ chứ không phải plumbing trạng thái.

Bước tiếp theo cho team đang chuyển đổi

Nếu codebase còn Vuex, bắt đầu từ việc tách một module duy nhất (auth, theme, hoặc cart) sang Pinia trong một sprint. Nếu Pinia đang nhồi server state, chọn một resource (ví dụ users) chuyển hết sang TanStack Query và đo bundle, đo số dòng code giảm. Cảm giác nhẹ nhõm sau lần chuyển đầu tiên thường đủ để thuyết phục cả team đi tiếp.

Nguồn tham khảo