Documents

Vue

Build, debug, and review Vue 3 code across reactivity, components, state, routing, forms, and performance.

What it does

Build, review, and troubleshoot Vue 3 components, composables, Pinia stores, forms, and Vue Router configuration. Diagnose reactivity loss, watcher loops, template refs, hydration mismatches, error handling, and render slowdowns using symptom-to-cause guidance, version-gated APIs, and concrete fixes. Also supports Vue 2 and Options API migration to Composition API.

When to use it

  • Debugging a watcher that loops or never fires
  • Fixing broken props, v-model, or Pinia state flow
  • Investigating hydration mismatches and blank pages
  • Migrating Vue 2 code to Composition API

The skill document

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.

Questions people ask

Can it diagnose state changes that never update the DOM?
Yes. It traces common reactivity loss from destructuring, reassigning reactive objects, passing snapshots to watchers, mishandling refs in arrays, or providing `.value` instead of the ref.
Does it cover Vue version differences?
Yes. Recommendations are gated by the installed Vue version, including `defineSlots` and generics in 3.3, `defineModel` in 3.4, and reactive props destructure and `useTemplateRef` in 3.5.
Is it suitable for Nuxt or Vite configuration issues?
No. Nuxt-specific auto-imports, data fetching, server routes, and SSR layers are out of scope, as are Vite plugin and build configuration.

Related skills

Build and review React code while diagnosing state, hook, rendering, hydration, and performance issues.

245 installs3 stars

Build, debug, review, and migrate Svelte and SvelteKit applications.

62 installs3 stars

Build, debug, optimize, test, and release Flutter apps with symptom-driven Dart guidance.

119 installs6 stars

Write, configure, migrate, and debug Tailwind CSS across utility markup, themes, variants, and builds.

129 installs5 stars

Write, debug, and review Kotlin across coroutines, flows, null safety, interop, Compose, builds, and tests.

84 installs2 stars

Diagnose, fix, test, and release native Android apps across build, device, runtime, and Play layers.

115 installs4 stars