文档

Vue

针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。

它能做什么

Vue 3 开发与调试参考,覆盖响应式(ref 与 reactive、watcher、computed)、组件与 composables、Pinia、Vue Router、表单、SSR 水合、scoped 样式、TypeScript、测试,以及 Vue 2 到 Vue 3 的迁移。把特定控制台警告与症状("Maximum recursive updates exceeded"、"Failed to resolve component"、"getActivePinia() was called"、template ref 为 null、未捕获异常导致页面空白)映射到对应修复方法。对宏的使用给出 Vue 版本门槛:defineModel 需要 3.4+,defineSlots 需要 3.3+,reactive props 解构需要 3.5+。不覆盖 Nuxt、Vite 构建配置以及其他前端框架。

什么时候用它

  • 排查 watcher 死循环或不触发
  • 把 Vue 2 Options API 组件迁移到 Composition API 与 script setup
  • 恢复因解构或重新赋值丢失的响应式
  • 修复 template ref 为 null 或 SSR 水合不匹配

技能文档

User preferences and memory live in ~/Clawic/data/vue/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/vue/ or ~/clawic/vue/), move it to ~/Clawic/data/vue/.

When To Use

  • Writing or reviewing Vue 3 components, composables, stores, and router configuration
  • Debugging: state changes that never reach the DOM, watchers that loop or never fire, null template refs, hydration mismatches, memory leaks, a page that goes blank after an uncaught error
  • Choosing between ref and reactive, computed and watch, props and provide/inject, Pinia and a plain composable
  • Performance work: slow lists, laggy typing, oversized bundles, components that re-render on every keystroke
  • Migrating Vue 2 / Options API code to Composition API and ``
  • Not for Nuxt's own layer (auto-imports, useFetch, server routes, ``) → nuxt; Vite plugin and build config → vite

Quick Reference

SituationPlay
State changes, DOM does notReactivity was lost or never established — walk the Reactivity Loss table below
Forgot .value and got a Ref object in a log.value in script, auto-unwrapped in template — but NOT inside arrays or Map/Set (arr[0].value)
state = {...} stopped workingreactive cannot be reassigned; use ref and set .value, or Object.assign(state, next)
Destructuring killed reactivitytoRefs(state), storeToRefs(store), or pass () => state.x — props destructure is safe only on vue >=3.5
"Maximum recursive updates exceeded"A watcher or computed mutates its own dependency → debug.md Infinite Loops
Watcher reads stale DOMDefault flush: 'pre' runs before render; use flush: 'post' or await nextTick() (rule 4)
Template ref is nullRefs bind at mount — read in onMounted, not in setup body (components.md)
Child does not receive updates through v-model:modelValue + @update:modelValue, or defineModel() on vue >=3.4 (forms.md)
Route param changes, component does not reloadVue Router reuses the instance — watch(() => route.params.id) (routing.md)
Chart / map / editor library misbehaves in a refDeep proxy broke instance identity — markRaw() or shallowRef() (rule 6)
Typing lags, list of thousands re-rendersshallowRef + v-memo + virtualize above virtualize_threshold (performance.md)
Hydration mismatch after SSRNon-deterministic render or invalid HTML nesting → ssr.md
Scoped style does not reach child markup:deep() selector; >>> and ::v-deep are the deprecated spellings (sfc.md)
vue-tsc errors that tsc never showed.vue files only type-check through vue-tsc (typescript.md)
Test asserts before the DOM updatesawait nextTick() / await flushPromises(); trigger() returns a promise (testing.md)
Vue 2 code, $on / filters / .sync goneAll removed in Vue 3 → migration.md
Page goes blank after a throw, one console errorNothing catches errors by default — a setup-phase throw unmounts the subtree (errors.md)
Async component stuck on its loader, or Failed to fetch dynamically imported moduletimeout + errorComponent + onError retry; a post-deploy chunk miss needs a guarded reload (errors.md)
Installing something app-wide: helper, component, directive, configA plugin's install(app); prefer app.provide over globalProperties (plugins.md)
Screen reader announces nothing after a route changeFocus stayed on the old page — move it to the new view's heading (routing.md)
Anything elseReproduce in an isolated SFC with no props and no store, then add one input at a time until it breaks

Depth on demand: debug.md symptom→cause chains · reactivity.md refs, watchers, effect scopes · components.md props, emits, slots, built-in components · composables.md reusable logic · templates.md directives and rendering · sfc.md script setup macros and scoped CSS · state.md Pinia, Vuex 4, provide/inject · routing.md Vue Router · forms.md inputs and validation · errors.md boundaries, the app error handler, failure states · plugins.md app.use, global config, registration · performance.md render and bundle cost · ssr.md hydration · typescript.md typing components · testing.md Test Utils, Vitest, Jest · migration.md Vue 2 to Vue 3 · security.md XSS and template injection.

Core Rules

  1. Pass the source, not the value. A watcher, computed, or composable receives a ref or a getter (() => props.id) — never the already-read value, which is a dead snapshot. Test: if deleting the arrow function still compiles, the reactivity is already gone. This single mistake explains most "my watcher never fires".
  2. ref by default; reactive only for a bounded object you never reassign. reactive fails four ways — reassignment breaks it, destructuring breaks it, it rejects primitives, and its ref-unwrapping stops inside arrays and Map/Set. ref has one cost (.value) and no cliffs.
  3. computed derives, watch acts. A computed is cached and must be pure: same dependencies in, same value out, zero side effects. The moment you need to fetch, write, or navigate, that is a watcher. A computed with a fetch inside runs an unpredictable number of times, because caching decides when it evaluates.
  4. Know the flush phase before you read the DOM. Watchers default to flush: 'pre' (before the component re-renders), so DOM reads see the previous frame. Order: sync (immediately, on every mutation) → pre (batched, before render) → post (after DOM patch). Reading layout or measuring elements requires post or await nextTick().
  5. Only what is registered synchronously in setup is cleaned up for you. Effects, watchers, and lifecycle hooks created before the first await are bound to the component's scope and stopped at unmount. Anything created inside a timer, a promise callback, or after await leaks — keep the stop handle, or wrap the work in effectScope() and dispose it (reactivity.md).
  6. markRaw or shallowRef for every non-plain object. Class instances (Chart.js, Leaflet, CodeMirror, WebSocket, IndexedDB handles, Web Audio nodes) put in ref()/reactive() get deep-proxied: the cost is proportional to the object graph, and instanceof plus === against the raw object start failing because the proxy is not the instance. Vue warns on components stored reactively; for library instances it stays silent and you get "the map went blank".
  7. :key is identity, not position. :key="index" on a list that reorders, filters, or splices makes Vue reuse the wrong component instance: the DOM shows row 3's text with row 5's checkbox state. Use a stable ID; use the index only for a list that is append-only and never sorted.
  8. Gate every macro on the installed version. defineSlots and generic components need vue >=3.3; defineModel needs vue >=3.4; useTemplateRef, useId, onWatcherCleanup, and reactive props destructure need vue >=3.5. Check package.json before recommending one — a macro that does not exist compiles to nothing and fails at runtime with an undefined identifier.

Console Warnings

Vue's warnings name the cause precisely; treat each as a lookup, not a puzzle. (Production builds strip them — reproduce in dev.)

WarningReal causeFix
Failed to resolve component: XNot registered, or a case/typo mismatch between import and tagImport in `` (auto-registers), or check PascalCase vs kebab-case
Extraneous non-props attributes ... could not be automatically inheritedComponent has multiple root nodes, so Vue cannot pick a fallthrough targetSingle root, or defineOptions({ inheritAttrs: false }) + explicit v-bind="$attrs"
Maximum recursive updates exceededAn effect writes to state it also readsIsolate the write behind a condition, or derive with computed (debug.md)
Invalid watch sourceA plain value was passed where a ref/getter belongsWrap in () => ... (rule 1)
Property "x" was accessed during render but is not definedNot returned from setup(), or typo; also fires when and a plain disagreeReturn it, or declare it in ``
Hydration node mismatchServer HTML ≠ first client renderssr.md — non-determinism or invalid nesting
inject() can only be used inside setup()Called in a callback, after await, or in a plain moduleCall synchronously at the top of setup and store the result
Set operation on key "x" failed: target is readonlyMutating a prop or a readonly() proxyEmit upward, or clone before editing
Vue received a Component that was made a reactive objectA component definition stored in ref/reactiveshallowRef for dynamic components; markRaw for anything class-like (rule 6)
Component provided template option but runtime compilation is not supportedRuntime-only build plus a string templateMove it to an SFC, or switch to the bundler build with the compiler (security.md — it needs unsafe-eval)
"getActivePinia()" was called but there was no active Pinia (Pinia <2.1 worded it getActivePinia was called with no active Pinia)Store used before app.use(pinia), or at module scopeCall the store inside setup or after install (state.md)

Reactivity Loss

Every "the UI doesn't update" bug is one row of this table. Diagnose by asking what you held onto: the source, or a value read from it.

You wroteWhat you holdWrite instead
const { count } = reactive(state)A number, frozen at read timetoRefs(state), or state.count at the point of use
const { items } = useStore() (Pinia)A frozen snapshot of state; actions survive destructuring, state does notstoreToRefs(store) for state, plain destructure for actions
state = { ...state, x: 1 } on a reactiveA new unproxied object; the old proxy still feeds the DOMObject.assign(state, patch), or hold a ref and swap .value
watch(props.id, ...)The current id, a primitivewatch(() => props.id, ...)
const first = arr.value[0] where items are refsA ref (arrays do not unwrap)arr.value[0].value, or store plain objects
provide('user', user.value)A snapshot; injectors never updateprovide('user', user) — provide the ref itself
const cfg = toRaw(state) then mutate cfgThe raw object; writes bypass trackingMutate the proxy; use toRaw only to hand data to a non-Vue library
Object.freeze(data) then ref(data)Frozen data Vue cannot proxy — silently non-reactiveFreeze after rendering, or use shallowRef deliberately
const { x } = defineProps() on vue <3.5A snapshot (the compiler transform only landed in 3.5)toRef(props, 'x'), or upgrade and keep the destructure

Component Communication

Pick the narrowest mechanism that reaches the target; every step outward costs traceability.

From → ToMechanismBreaks down when
Parent → childProps (defineProps)The chain is 3+ levels — prop drilling; move to provide/inject
Child → parentEmits (defineEmits)You need a return value; then it is a callback prop, not an event
Two-way on one valuedefineModel() (vue >=3.4), else modelValue + update:modelValueMultiple values — use named models (v-model:title)
Ancestor → deep descendantprovide / inject with an InjectionKeyConsumers outside that subtree, or you need writes from anywhere → store
Parent → child imperative callTemplate ref + defineExposeYou are reaching in to read state you should have passed down
Slot content → slot ownerScoped slot propsThe content needs the owner's lifecycle, not just its data
Siblings / any-to-anyPinia storeIt is one screen's local concern — lift state to the common parent instead
Across routesRoute params for identity, store for payloadYou are stuffing objects in query strings (routing.md)
Anything elsePinia store, then narrow it once the shape is known

Output Gates

Before emitting a component, composable, or store, verify:

  • Every reactive source crossing a function boundary is a ref or a getter, never a read value?
  • Each v-for has a stable identity :key, and no v-if on the same element?
  • Watchers and listeners created outside synchronous setup have a stop handle or an effectScope?
  • Props treated as read-only; every mutation routed through an emit or a model?
  • Any class instance or third-party object wrapped in markRaw/shallowRef?
  • Every macro used exists in the project's Vue version (rule 8)?
  • No user-controlled string reaching v-html, :is, or :href (security.md)?
  • Every async region has a loading, an error, and an empty state, and something above it catches a throw (errors.md)?

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/vue/config.yaml.

VariableTypeDefaultEffect
vue_version3.2 | 3.3 | 3.4 | 3.5 | later3.5Gates which macros and APIs get recommended (rule 8) and which workarounds appear in examples; 3.2 is the supported floor (sfc.md), and later means assume everything through 3.5 and check the release notes before using a newer API
api_stylescript-setup | setup-function | optionsscript-setupShapes every code sample and the migration.md target style
languagets | jstsWhether examples carry type annotations and whether typescript.md guidance is volunteered
state_librarypinia | vuex | nonepiniaSelects the store patterns in state.md; vuex switches to its Vuex 4 section (useStore, commit/dispatch, namespaced paths); none routes shared state to composables and provide/inject
rendering_modespa | ssr | ssgspaTurns on SSR-safety review (module-scope state, browser APIs, hydration) from ssr.md
virtualize_thresholdnumber (rows)200Row count above which performance.md recommends virtual scrolling instead of v-memo
test_runnervitest | jest | nonevitestShapes the setup and mocking examples in testing.md; jest swaps vi.* for jest.* and adds the transform and transformIgnorePatterns config that Vitest does not need

Preference areas to record as the user reveals them:

  • conventionsref vs reactive house style, component and composable naming, SFC block order, feature-folder vs type-folder layout
  • tooling — build tool, package manager, UI kit, validation and i18n libraries, whether VueUse is welcome or the project prefers hand-rolled composables
  • safety posturev-html policy, whether to run codemods without confirmation, how loudly to flag missing keys and unbounded watchers
  • output format — whether components ship with a test, how much explanation accompanies code, comment density
  • platform — browser support target, CSP strictness (which decides runtime-only vs compiler build), SSR host

Traps

TrapWhy it failsDo instead
v-if and v-for on the same elementIn Vue 3 v-if evaluates first and cannot see the loop variable — "Property 'item' is not defined" (the opposite of Vue 2's precedence)`` wrapping the v-if, or filter in a computed
v-html with anything a user typedVue escapes mustaches but never v-html — this is the XSS doorRender as text, or sanitize server-side (security.md)
watch(obj, ...) on a reactive objectVue forces deep: true and the callback gets the same object as old and new valueWatch a getter of the field you care about
deep: true on a large treeTraversal cost is proportional to the node count, on every mutationWatch specific getters, or deep: 2 on vue >=3.5
Mutating a prop object because "it works"It does mutate — same reference — but the parent has no record and DevTools shows no sourcedefineModel() or an explicit emit
async setup() without ``The component never resolves and renders nothing, silentlySync setup + a loading ref, or an explicit `` boundary
Registering a listener in onMounted with no onUnmountedSurvives the component; every remount adds anotherPair them, or use a composable that owns the cleanup
Module-scope const state = reactive({}) in SSRThe module is shared across requests — one user's data leaks into another's pageState factory per request (ssr.md)
KeepAlive without maxEvery visited view stays in memory for the session`` and onActivated for refresh
Index as :key on a sortable listInstance reuse mixes rendered content with stale local stateStable ID (rule 7)

Where Experts Disagree

  • ref everywhere vs reactive for objects. The ref-only camp wins on consistency and on never hitting the reassignment cliff; the reactive camp wins on template and script ergonomics for a form-shaped object. Default: ref, with reactive allowed for an object that is created once and mutated in place. Do not mix styles inside one module.
  • Pinia store vs a shared composable. A composable holding module-scope refs is a global store with no devtools, no SSR safety, and no plugin surface. Reach for it only for a singleton with no server rendering; anything crossing routes or touched by more than two features belongs in a store (state.md).
  • Options API is legacy. It is supported indefinitely and still the fastest onboarding for a large team; Composition API's real payoff is logic extraction and typing. Boundary: mixins and cross-cutting logic mean migrate; a stable CRUD screen in Options API is not technical debt.
  • VueUse vs hand-rolled composables. Depending on a 200-utility library for useLocalStorage is a genuine cost; reimplementing useIntersectionObserver correctly (SSR guard, cleanup, ref-or-value input) is a bigger one. Split on whether the utility touches a browser API lifecycle.

More Clawic skills, get them at https://clawic.com/skills/vue (install if the user confirms):

  • nuxt — Nuxt's SSR, file routing, data fetching, and server layer
  • vite — build, dev server, and bundle configuration
  • typescript — type-system design beyond component typing
  • javascript — language-level semantics (async, coercion, closures)
  • playwright — end-to-end tests that a component test cannot cover

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/vue.

常见问题

这个 skill 是否覆盖 Nuxt 的 SSR、路由与数据获取?
不覆盖。Nuxt 自身那一层(自动导入、useFetch、服务端路由)由独立的 nuxt skill 处理;本 skill 直接覆盖 Vue 3、Vue Router 和 Pinia。
能否协助处理 Vite 构建配置?
不能。Vite 插件与构建配置不在范围内,相关问题请使用 vite skill。
宏的推荐按哪个 Vue 版本划线?
defineSlots 与泛型组件需要 3.3+,defineModel 需要 3.4+,useTemplateRef、useId、onWatcherCleanup 以及 reactive props 解构需要 3.5+。不存在的宏会编译为空、运行时报未定义标识符,所以推荐前应先核对 package.json 中的版本。

相关技能

按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。

245 次安装3 星标

排查并修复 Svelte 与 SvelteKit 的响应式、SSR 与构建问题,并把 Svelte 4 代码迁移到 runes。

62 次安装3 星标

把 Flutter 框架异常读成具体修复动作,再把 release 包真正跑到设备上。

119 次安装6 星标

诊断并交付能在生产环境编译生效的 Tailwind v3/v4 标记、主题与构建配置

129 次安装5 星标

为 Kotlin 代码提供正确性与惯用法诊断:空安全、协程、Flow、Compose 状态、Java 互操作与构建问题。

84 次安装2 星标

先把 Android 问题归到四层之一——构建、安装、运行时、商店——再对症修复。

115 次安装4 星标