原始内容
name: Vue slug: vue version: 1.0.3 description: >- Builds, debugs, and reviews Vue 3 apps: reactivity, components, composables, Pinia state, Vue Router, forms, and performance. Use when the UI does not update after a state change, a reactive object loses reactivity on destructure or reassignment, .value is forgotten, a watcher never fires or loops ("Maximum recursive updates exceeded"), props or v-model stop syncing, a template ref is null, SSR reports a hydration mismatch, a route param change does not reload the component, Pinia warns "getActivePinia() was called but there was no active Pinia", the console shows "Failed to resolve component" or "Extraneous non-props attributes", a thrown error blanks the page, a long list or large object makes typing lag, a chart or map library breaks after being put in ref(), scoped styles do not reach a child, or when migrating Vue 2 Options API to Composition API and script setup. Not for Nuxt-specific SSR, routing, and data fetching (nuxt), Vite build configuration (vite), or React and Svelte (react, svelte). homepage: https://clawic.com/skills/vue changelog: "Display name shown correctly" metadata: clawdbot: emoji: 💚 requires: bins: - node os: - linux - darwin - win32 displayName: Vue configPaths: - ~/Clawic/data/vue/
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
refandreactive, 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
<script setup> - Not for Nuxt's own layer (auto-imports,
useFetch, server routes,<ClientOnly>) →nuxt; Vite plugin and build config →vite
Quick Reference
| Situation | Play |
|---|---|
| State changes, DOM does not | Reactivity 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 working |
reactive cannot be reassigned; use ref and set .value, or Object.assign(state, next) |
| Destructuring killed reactivity | toRefs(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 DOM | Default flush: 'pre' runs before render; use flush: 'post' or await nextTick() (rule 4) |
Template ref is null |
Refs 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 reload | Vue Router reuses the instance — watch(() => route.params.id) (routing.md) |
Chart / map / editor library misbehaves in a ref |
Deep proxy broke instance identity — markRaw() or shallowRef() (rule 6) |
| Typing lags, list of thousands re-renders | shallowRef + v-memo + virtualize above virtualize_threshold (performance.md) |
| Hydration mismatch after SSR | Non-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 updates | await nextTick() / await flushPromises(); trigger() returns a promise (testing.md) |
Vue 2 code, $on / filters / .sync gone |
All removed in Vue 3 → migration.md |
| Page goes blank after a throw, one console error | Nothing 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 module |
timeout + errorComponent + onError retry; a post-deploy chunk miss needs a guarded reload (errors.md) |
| Installing something app-wide: helper, component, directive, config | A plugin's install(app); prefer app.provide over globalProperties (plugins.md) |
| Screen reader announces nothing after a route change | Focus stayed on the old page — move it to the new view's heading (routing.md) |
| Anything else | Reproduce 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
- 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". refby default;reactiveonly for a bounded object you never reassign.reactivefails four ways — reassignment breaks it, destructuring breaks it, it rejects primitives, and its ref-unwrapping stops inside arrays and Map/Set.refhas one cost (.value) and no cliffs.computedderives,watchacts. 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 afetchinside runs an unpredictable number of times, because caching decides when it evaluates.- 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 requirespostorawait nextTick(). - Only what is registered synchronously in setup is cleaned up for you. Effects, watchers, and lifecycle hooks created before the first
awaitare bound to the component's scope and stopped at unmount. Anything created inside a timer, a promise callback, or afterawaitleaks — keep the stop handle, or wrap the work ineffectScope()and dispose it (reactivity.md). markRaworshallowReffor every non-plain object. Class instances (Chart.js, Leaflet, CodeMirror, WebSocket, IndexedDB handles, Web Audio nodes) put inref()/reactive()get deep-proxied: the cost is proportional to the object graph, andinstanceofplus===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".:keyis 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.- Gate every macro on the installed version.
defineSlotsand generic components needvue >=3.3;defineModelneedsvue >=3.4;useTemplateRef,useId,onWatcherCleanup, and reactive props destructure needvue >=3.5. Checkpackage.jsonbefore 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.)
| Warning | Real cause | Fix |
|---|---|---|
Failed to resolve component: X |
Not registered, or a case/typo mismatch between import and tag | Import in <script setup> (auto-registers), or check PascalCase vs kebab-case |
Extraneous non-props attributes ... could not be automatically inherited |
Component has multiple root nodes, so Vue cannot pick a fallthrough target | Single root, or defineOptions({ inheritAttrs: false }) + explicit v-bind="$attrs" |
Maximum recursive updates exceeded |
An effect writes to state it also reads | Isolate the write behind a condition, or derive with computed (debug.md) |
Invalid watch source |
A plain value was passed where a ref/getter belongs | Wrap in () => ... (rule 1) |
Property "x" was accessed during render but is not defined |
Not returned from setup(), or typo; also fires when <script setup> and a plain <script> disagree |
Return it, or declare it in <script setup> |
Hydration node mismatch |
Server HTML ≠ first client render | ssr.md — non-determinism or invalid nesting |
inject() can only be used inside setup() |
Called in a callback, after await, or in a plain module |
Call synchronously at the top of setup and store the result |
Set operation on key "x" failed: target is readonly |
Mutating a prop or a readonly() proxy |
Emit upward, or clone before editing |
Vue received a Component that was made a reactive object |
A component definition stored in ref/reactive |
shallowRef for dynamic components; markRaw for anything class-like (rule 6) |
Component provided template option but runtime compilation is not supported |
Runtime-only build plus a string template | Move 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 scope |
Call 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 wrote | What you hold | Write instead |
|---|---|---|
const { count } = reactive(state) |
A number, frozen at read time | toRefs(state), or state.count at the point of use |
const { items } = useStore() (Pinia) |
A frozen snapshot of state; actions survive destructuring, state does not | storeToRefs(store) for state, plain destructure for actions |
state = { ...state, x: 1 } on a reactive |
A new unproxied object; the old proxy still feeds the DOM | Object.assign(state, patch), or hold a ref and swap .value |
watch(props.id, ...) |
The current id, a primitive | watch(() => props.id, ...) |
const first = arr.value[0] where items are refs |
A ref (arrays do not unwrap) | arr.value[0].value, or store plain objects |
provide('user', user.value) |
A snapshot; injectors never update | provide('user', user) — provide the ref itself |
const cfg = toRaw(state) then mutate cfg |
The raw object; writes bypass tracking | Mutate 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-reactive | Freeze after rendering, or use shallowRef deliberately |
const { x } = defineProps() on vue <3.5 |
A 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 → To | Mechanism | Breaks down when |
|---|---|---|
| Parent → child | Props (defineProps) |
The chain is 3+ levels — prop drilling; move to provide/inject |
| Child → parent | Emits (defineEmits) |
You need a return value; then it is a callback prop, not an event |
| Two-way on one value | defineModel() (vue >=3.4), else modelValue + update:modelValue |
Multiple values — use named models (v-model:title) |
| Ancestor → deep descendant | provide / inject with an InjectionKey |
Consumers outside that subtree, or you need writes from anywhere → store |
| Parent → child imperative call | Template ref + defineExpose |
You are reaching in to read state you should have passed down |
| Slot content → slot owner | Scoped slot props | The content needs the owner's lifecycle, not just its data |
| Siblings / any-to-any | Pinia store | It is one screen's local concern — lift state to the common parent instead |
| Across routes | Route params for identity, store for payload | You are stuffing objects in query strings (routing.md) |
| Anything else | Pinia 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-forhas a stable identity:key, and nov-ifon 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.
| Variable | Type | Default | Effect |
|---|---|---|---|
| vue_version | 3.2 | 3.3 | 3.4 | 3.5 | later | 3.5 | Gates 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_style | script-setup | setup-function | options | script-setup | Shapes every code sample and the migration.md target style |
| language | ts | js | ts | Whether examples carry type annotations and whether typescript.md guidance is volunteered |
| state_library | pinia | vuex | none | pinia | Selects 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_mode | spa | ssr | ssg | spa | Turns on SSR-safety review (module-scope state, browser APIs, hydration) from ssr.md |
| virtualize_threshold | number (rows) | 200 | Row count above which performance.md recommends virtual scrolling instead of v-memo |
| test_runner | vitest | jest | none | vitest | Shapes 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:
- conventions —
refvsreactivehouse 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 posture —
v-htmlpolicy, 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
| Trap | Why it fails | Do instead |
|---|---|---|
v-if and v-for on the same element |
In Vue 3 v-if evaluates first and cannot see the loop variable — "Property 'item' is not defined" (the opposite of Vue 2's precedence) |
<template v-for> wrapping the v-if, or filter in a computed |
v-html with anything a user typed |
Vue escapes mustaches but never v-html — this is the XSS door |
Render as text, or sanitize server-side (security.md) |
watch(obj, ...) on a reactive object |
Vue forces deep: true and the callback gets the same object as old and new value |
Watch a getter of the field you care about |
deep: true on a large tree |
Traversal cost is proportional to the node count, on every mutation | Watch 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 source | defineModel() or an explicit emit |
async setup() without <Suspense> |
The component never resolves and renders nothing, silently | Sync setup + a loading ref, or an explicit <Suspense> boundary |
Registering a listener in onMounted with no onUnmounted |
Survives the component; every remount adds another | Pair them, or use a composable that owns the cleanup |
Module-scope const state = reactive({}) in SSR |
The module is shared across requests — one user's data leaks into another's page | State factory per request (ssr.md) |
KeepAlive without max |
Every visited view stays in memory for the session | <KeepAlive :max="10"> and onActivated for refresh |
Index as :key on a sortable list |
Instance reuse mixes rendered content with stale local state | Stable ID (rule 7) |
Where Experts Disagree
refeverywhere vsreactivefor objects. Theref-only camp wins on consistency and on never hitting the reassignment cliff; thereactivecamp wins on template and script ergonomics for a form-shaped object. Default:ref, withreactiveallowed 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
useLocalStorageis a genuine cost; reimplementinguseIntersectionObservercorrectly (SSR guard, cleanup, ref-or-value input) is a bigger one. Split on whether the utility touches a browser API lifecycle.
Related Skills
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 layervite— build, dev server, and bundle configurationtypescript— type-system design beyond component typingjavascript— language-level semantics (async, coercion, closures)playwright— end-to-end tests that a component test cannot cover
Feedback
- If useful, star it: https://clawic.com/skills/vue
- Latest version: https://clawic.com/skills/vue
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/vue.