vue-x-2

内容来源:clawhub · 原始地址 · 查看安装指南

原始内容


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 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 <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

  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.)

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-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.

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:

  • 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

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

  • 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.

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 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.