Vue.js Knowledge Patch
Use this patch when changing Vue applications or ecosystem tooling, especially projects using Vue Router, Pinia, Pinia Colada, Nuxt, or Vite integrations.
Reference index
| Reference | Topics |
|---|---|
references/vue-router-5.md |
Router package migration, file-based routes, typed routes, parameter parsing, data loaders, guards, scrolling, and distribution changes |
references/pinia-3.md |
Pinia package requirements, ESM migration, Devtools, refs, hydration, public integration APIs, and Nuxt compatibility |
references/pinia-colada-queries.md |
Query state, keys, cache operations, pagination, infinite queries, callbacks, metadata, and query plugins |
references/pinia-colada-mutations-and-integration.md |
Mutations, optimistic updates, shared definitions, persistence, SSR, testing, migrations, and extension API |
references/vue-core-and-vapor.md |
Typed template refs, release channels, hydration, SSR, custom elements, slots, transitions, and component types |
references/ecosystem-tooling.md |
Nuxt custom fetchers, Vite's Rolldown transition, and Vite+ |
Breaking changes and migrations
Move file-based routing imports into Vue Router
Vue Router 5 includes the former unplugin-vue-router functionality. Remove
that dependency and update imports:
import VueRouter from 'vue-router/vite'
import type { EditableTreeNode, Options } from 'vue-router/unplugin'
Other build adapters and utilities, including resolveOptions, are exported
from vue-router/unplugin. Applications that did not use the old plugin
generally need no source changes.
Remove the old unplugin-vue-router/client type reference. Emit generated
declarations under src so ordinary TypeScript includes find them:
VueRouter({ dts: 'src/routes.d.ts' })
For route-aware SFC typing, use the bundled Volar plugins:
{
"compilerOptions": { "rootDir": "." },
"vueCompilerOptions": {
"plugins": [
"vue-router/volar/sfc-typed-router",
"vue-router/volar/sfc-route-blocks"
]
}
}
The typed-router plugin infers a page's route from its file location, including
the types returned by no-argument useRoute() and template $route.
Treat experimental routing as ESM-only
Do not load vue-router/experimental from CommonJS. Vite is only an optional
peer dependency, so non-Vite installations do not need it.
Typed query parameters are optional. Invalid formats warn and are filtered
instead of failing route matching, and a query value may be undefined. Do
not assume a declared query key is present.
Upgrade Pinia packaging deliberately
Pinia 3 requires TypeScript 4.5 or newer because its declarations use native
Awaited. Its package declares "type": "module" while continuing to ship
CommonJS distribution files. Its standalone IIFE build does not contain Vue
Devtools, so include Devtools separately when that distribution needs it.
Pinia 4 is ESM-only and requires @vue/devtools-api v8 as a separately
installed package. Move CommonJS-only tooling to an ESM-capable path first:
pnpm add pinia@^4 @vue/devtools-api@^8
Vue Router supports Pinia 4. For Nuxt 5, use a compatible @pinia/nuxt
release as detailed in the Pinia reference.
Apply Pinia Colada API migrations
Current useQuery() accepts one options object. Older two-argument
useQuery/useQueryState forms are removed, and global query defaults belong
under queryOptions. The package includes ast-grep migration rules; commit
work first, then run the matching installed rule against source files:
pnpm --package=@ast-grep/cli dlx ast-grep scan \
-r node_modules/@pinia/colada/codemods/rules/migration-0-21-to-1-0.yaml \
-i src
Router data loaders
Install DataLoaderPlugin before the router so it participates in initial
navigation:
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'
import { DataLoaderPlugin } from 'vue-router/experimental'
const router = createRouter({ history: createWebHistory(), routes })
const app = createApp({})
app.use(DataLoaderPlugin, { router })
app.use(router)
Loaders execute outside component setup, are collected and awaited during
navigation, and support parallel deduplicated fetching, loading/error state,
SSR, and prefetching. Basic loaders always rerun; the Colada implementation
uses @pinia/colada.
Export a loader from its page component. If it lives elsewhere, re-export it from the page so the router can discover it:
<script lang="ts">
import { defineBasicLoader } from 'vue-router/experimental'
export const useUserData = defineBasicLoader('/users/[id]', (route) =>
getUserById(route.params.id),
)
</script>
<script setup lang="ts">
const { data, isLoading, error, reload } = useUserData()
</script>
Navigation waits for the loader. Other callers share its fetching instance.
Pinia Colada essentials
Register Pinia first, then Pinia Colada. Plugin factories run in array order:
app.use(createPinia())
app.use(PiniaColada, {
queryOptions: { staleTime: 5_000, gcTime: 300_000 },
mutationOptions: {},
plugins: [],
})
The default query freshness is 5 seconds and garbage-collection time is 5
minutes. A query needs a serializable array key and a query function:
const query = useQuery({
key: () => ['products', id.value],
query: () => getProduct(id.value),
enabled: () => Boolean(id.value),
})
Every reactive query input must occur in the reactive key. Array order matters;
object-property order does not, and undefined object properties are removed.
Disable long-lived queries when their route inputs are unavailable.
Read the two state axes independently:
| Field | Values | Meaning |
|---|---|---|
status |
pending, success, error |
Data state |
asyncStatus |
idle, loading |
Request activity |
refresh() deduplicates and respects freshness. refetch() forces a request.
Both resolve to state by default; pass true to rethrow an error.
Use defineQueryOptions() for reusable, parameterized, type-tagged keys. Use
defineQuery() only for once-instantiated globally shared composables; extra
state it returns is not serialized for SSR. A query placed in a long-lived
Pinia store is effectively immortal.
Cache and mutation safety
invalidateQueries() marks matching entries stale and refetches active entries
by default. Pass 'all' as its second argument to include inactive matches.
Use ensure() before seeding when the cache entry must preserve query options
and freshness behavior.
mutate() catches failures and returns nothing; mutateAsync() returns a
rejecting promise. Awaited mutation hooks keep loading active, and the value
returned by onMutate becomes the context for later hooks.
For an optimistic update:
- Snapshot and replace cached data in
onMutate. - Call
cancelQueries()so stale results are discarded without refetching. - Return old and optimistic values as rollback context.
- Roll back only if the cache still holds this mutation's optimistic value.
- Invalidate the affected query on settlement.
Queries intentionally have no local success/error/settled callbacks. Watch
state for component effects or install PiniaColadaQueryHooksPlugin for global
fetch hooks. Query meta is fixed when the entry is created and must be
serializable for SSR.
Typed template refs
Use Vue's exported TemplateRef when a template ref needs an explicit type:
import { useTemplateRef, type TemplateRef } from 'vue'
const input: TemplateRef<HTMLInputElement> =
useTemplateRef<HTMLInputElement>('input')
Verification checklist
- Exercise kept-alive route reactivation: guards run when the route changes.
- Validate generated routes and parameter parsers; missing parsers throw.
- Test overlapping navigation and ensure stale async scrolling is ignored.
- Test query loading separately from data status, including preserved data after refetch errors.
- Use real
createPinia()in Pinia Colada component tests, then flush promises. - Await cache restoration before mounting when using asynchronous persistence.
- Test hydration of text inputs, namespaced elements, and Pinia collections.