{"slug":"vue-ops","title":"vue-ops","summary":"Vue 3 development patterns, Composition API, Pinia state management, Vue Router, and Nuxt 4. Use for: vue, vuejs, composition api, pinia, vue router, nuxt, nuxt4, nuxt3, script setup, composable, reactive, defineProps, defineEmits, defineModel, v-model, provide inject, vue3.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-30T19:37:29.474419Z","repo":{"url":"https://github.com/0xDarkMatter/claude-mods","stars":43,"forks":7,"license":"MIT","updatedAt":"2026-09-30T15:18:48Z"},"bodyHtml":"<hr>\n<h2>name: vue-ops\ndescription: \"Vue 3 development patterns, Composition API, Pinia state management, Vue Router, and Nuxt 4. Use for: vue, vuejs, composition api, pinia, vue router, nuxt, nuxt4, nuxt3, script setup, composable, reactive, defineProps, defineEmits, defineModel, v-model, provide inject, vue3.\"\nlicense: MIT\nallowed-tools: \"Read Write Bash\"\nmetadata:\nauthor: claude-mods\nrelated-skills: typescript-ops, testing-ops, tailwind-ops, javascript-ops</h2>\n<h1>Vue Operations</h1>\n<p>Comprehensive Vue 3 reference covering Composition API, Pinia, Vue Router, Nuxt 4, and testing — production patterns with TypeScript throughout.</p>\n<blockquote>\n<p>Vue 3 / Nuxt 4 ecosystem facts verified as of 2026-07-05.</p>\n</blockquote>\n<hr>\n<h2>Reactivity Decision Tree</h2>\n<pre><code>What data do I need to make reactive?\n│\n├─ A single primitive (string, number, boolean)?\n│   └─ ref()\n│       const count = ref(0)\n│       const name = ref('')\n│\n├─ A plain object or array with deep reactivity?\n│   ├─ Will I destructure it or pass properties individually?\n│   │   └─ reactive() — but use toRefs() when destructuring\n│   └─ Will I replace the whole object at once?\n│       └─ ref() — ref.value = newObject\n│\n├─ Derived/computed state from other reactive sources?\n│   └─ computed()\n│       const doubled = computed(() =&gt; count.value * 2)\n│\n├─ A large object where only top-level keys change?\n│   └─ shallowRef() or shallowReactive()\n│       const state = shallowRef({ nested: { big: 'data' } })\n│\n├─ Side effects that should run when dependencies change?\n│   ├─ Don't need to know old value, auto-tracks dependencies?\n│   │   └─ watchEffect(() =&gt; { ... })\n│   └─ Need old/new values, explicit sources, or lazy execution?\n│       └─ watch(source, (newVal, oldVal) =&gt; { ... })\n│\n└─ Data that should NOT be reactive (raw DOM, third-party instances)?\n    └─ markRaw(obj) or shallowRef(obj)\n</code></pre>\n<hr>\n<h2>Component Communication Decision Tree</h2>\n<pre><code>How far does data need to travel?\n│\n├─ Parent → direct child?\n│   └─ props (defineProps)\n│       Direct, explicit, type-safe\n│\n├─ Child → parent (user action / data update)?\n│   └─ emit (defineEmits)\n│       defineEmits&lt;{ change: [value: string] }&gt;()\n│\n├─ Parent ↔ child bidirectional binding?\n│   └─ v-model via defineModel() (Vue 3.4+)\n│       const model = defineModel&lt;string&gt;()\n│\n├─ Ancestor → deep descendant (prop drilling problem)?\n│   └─ provide / inject\n│       Use InjectionKey&lt;T&gt; for type safety\n│\n├─ Siblings or unrelated components?\n│   ├─ Simple/few shared values?\n│   │   └─ provide / inject from a common ancestor\n│   └─ Complex shared state or cross-tree communication?\n│       └─ Pinia store\n│\n├─ Truly global state (user session, cart, preferences)?\n│   └─ Pinia store\n│       defineStore with setup syntax\n│\n└─ One-time events between distant components (rare)?\n    └─ Pinia action + watch, or mitt event bus\n        Avoid: Vue removed $emit on root in Vue 3\n</code></pre>\n<hr>\n<h2>Composition API Quick Reference</h2>\n<h3><code>&lt;script setup&gt;</code> — the standard</h3>\n<pre><code>&lt;script setup lang=\"ts\"&gt;\nimport { ref, computed, watch, onMounted } from 'vue'\n\n// Props — with TypeScript generics (no runtime declaration needed)\nconst props = defineProps&lt;{\n  title: string\n  count?: number\n}&gt;()\n\n// Props with defaults\nconst props = withDefaults(defineProps&lt;{\n  size: 'sm' | 'md' | 'lg'\n  disabled?: boolean\n}&gt;(), {\n  size: 'md',\n  disabled: false,\n})\n\n// Emits — type-safe event signatures\nconst emit = defineEmits&lt;{\n  change: [value: string]        // named tuple syntax (Vue 3.3+)\n  update: [id: number, data: object]\n  close: []\n}&gt;()\n\n// Reactive state\nconst count = ref(0)\nconst user = reactive({ name: '', email: '' })\n\n// Computed\nconst doubled = computed(() =&gt; count.value * 2)\n\n// Watch\nwatch(count, (newVal, oldVal) =&gt; {\n  console.log(`count changed from ${oldVal} to ${newVal}`)\n})\n\n// Lifecycle\nonMounted(() =&gt; {\n  console.log('component mounted')\n})\n&lt;/script&gt;\n</code></pre>\n<h3><code>defineModel</code> — v-model binding (Vue 3.4+)</h3>\n<pre><code>&lt;!-- Child component: MyInput.vue --&gt;\n&lt;script setup lang=\"ts\"&gt;\nconst model = defineModel&lt;string&gt;({ required: true })\n\n// Named v-model: &lt;MyInput v-model:title=\"...\" /&gt;\nconst title = defineModel&lt;string&gt;('title')\n\n// With modifiers\nconst [modelValue, modifiers] = defineModel&lt;string, 'trim' | 'uppercase'&gt;()\n&lt;/script&gt;\n\n&lt;template&gt;\n  &lt;input :value=\"model\" @input=\"model = $event.target.value\" /&gt;\n&lt;/template&gt;\n</code></pre>\n<h3><code>defineExpose</code> — expose to parent refs</h3>\n<pre><code>&lt;script setup lang=\"ts\"&gt;\nconst inputRef = ref&lt;HTMLInputElement | null&gt;(null)\n\nfunction focus() {\n  inputRef.value?.focus()\n}\n\n// Expose public API for parent template refs\ndefineExpose({ focus })\n&lt;/script&gt;\n</code></pre>\n<h3><code>defineOptions</code> — component meta (Vue 3.3+)</h3>\n<pre><code>&lt;script setup lang=\"ts\"&gt;\ndefineOptions({\n  name: 'MyComponent',\n  inheritAttrs: false,\n})\n&lt;/script&gt;\n</code></pre>\n<h3><code>defineSlots</code> — type slots (Vue 3.3+)</h3>\n<pre><code>&lt;script setup lang=\"ts\"&gt;\ndefineSlots&lt;{\n  default(props: { item: User }): any\n  header(props: {}): any\n}&gt;()\n&lt;/script&gt;\n</code></pre>\n<hr>\n<h2>Pinia Quick Start</h2>\n<h3>Setup syntax (recommended — composable style)</h3>\n<pre><code>// stores/counter.ts\nimport { defineStore } from 'pinia'\nimport { ref, computed } from 'vue'\n\nexport const useCounterStore = defineStore('counter', () =&gt; {\n  // state\n  const count = ref(0)\n  const name = ref('Counter')\n\n  // getters\n  const doubled = computed(() =&gt; count.value * 2)\n\n  // actions\n  function increment() {\n    count.value++\n  }\n\n  async function fetchData() {\n    const data = await api.get('/data')\n    count.value = data.total\n  }\n\n  return { count, name, doubled, increment, fetchData }\n})\n</code></pre>\n<h3>Options syntax</h3>\n<pre><code>export const useCounterStore = defineStore('counter', {\n  state: () =&gt; ({ count: 0 }),\n  getters: {\n    doubled: (state) =&gt; state.count * 2,\n  },\n  actions: {\n    increment() { this.count++ },\n  },\n})\n</code></pre>\n<h3>Using stores in components</h3>\n<pre><code>&lt;script setup lang=\"ts\"&gt;\nimport { storeToRefs } from 'pinia'\nimport { useCounterStore } from '@/stores/counter'\n\nconst store = useCounterStore()\n\n// storeToRefs preserves reactivity when destructuring state/getters\n// Actions can be destructured directly (they're not reactive)\nconst { count, doubled } = storeToRefs(store)\nconst { increment } = store\n&lt;/script&gt;\n</code></pre>\n<h3>Pinia plugins — persistence example</h3>\n<pre><code>// main.ts\nimport { createPinia } from 'pinia'\nimport piniaPluginPersistedstate from 'pinia-plugin-persistedstate'\n\nconst pinia = createPinia()\npinia.use(piniaPluginPersistedstate)\n\n// In store:\nexport const useAuthStore = defineStore('auth', () =&gt; { ... }, {\n  persist: true, // or { storage: sessionStorage, paths: ['token'] }\n})\n</code></pre>\n<hr>\n<h2>Vue Router Quick Reference</h2>\n<h3>Basic configuration</h3>\n<pre><code>// router/index.ts\nimport { createRouter, createWebHistory } from 'vue-router'\n\nconst router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes: [\n    {\n      path: '/',\n      name: 'home',\n      component: () =&gt; import('@/views/HomeView.vue'), // lazy load\n    },\n    {\n      path: '/users/:id',\n      name: 'user',\n      component: () =&gt; import('@/views/UserView.vue'),\n      props: true,                    // passes :id as prop\n      meta: { requiresAuth: true },\n    },\n    {\n      path: '/admin',\n      component: () =&gt; import('@/layouts/AdminLayout.vue'),\n      children: [\n        { path: '', component: () =&gt; import('@/views/admin/Dashboard.vue') },\n        { path: 'users', component: () =&gt; import('@/views/admin/Users.vue') },\n      ],\n    },\n    { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },\n  ],\n  scrollBehavior(to, from, savedPosition) {\n    if (savedPosition) return savedPosition\n    if (to.hash) return { el: to.hash, behavior: 'smooth' }\n    return { top: 0 }\n  },\n})\n\nexport default router\n</code></pre>\n<h3>Navigation guards</h3>\n<pre><code>// Global guard — auth check\nrouter.beforeEach((to, from) =&gt; {\n  const auth = useAuthStore()\n  if (to.meta.requiresAuth &amp;&amp; !auth.isLoggedIn) {\n    return { name: 'login', query: { redirect: to.fullPath } }\n  }\n})\n\n// Per-route guard\n{\n  path: '/admin',\n  beforeEnter: (to, from) =&gt; {\n    if (!isAdmin()) return { name: 'forbidden' }\n  },\n}\n</code></pre>\n<pre><code>&lt;!-- In-component guard --&gt;\n&lt;script setup lang=\"ts\"&gt;\nimport { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'\n\nonBeforeRouteLeave((to, from) =&gt; {\n  if (hasUnsavedChanges.value) {\n    return confirm('Leave without saving?')\n  }\n})\n&lt;/script&gt;\n</code></pre>\n<h3>TypeScript meta typing</h3>\n<pre><code>// router/index.ts — augment RouteMeta\ndeclare module 'vue-router' {\n  interface RouteMeta {\n    requiresAuth?: boolean\n    title?: string\n    breadcrumb?: string\n  }\n}\n</code></pre>\n<hr>\n<h2>Nuxt 4 Decision Tree</h2>\n<p>Nuxt 4's flagship change over Nuxt 3 is the <code>app/</code> source directory (app code separated\nfrom <code>server/</code> and root config — see <a href=\"./references/nuxt.md\">./references/nuxt.md</a>); the\nrendering strategies below are unchanged.</p>\n<pre><code>What rendering strategy does my app need?\n│\n├─ Public content (blogs, marketing, docs)?\n│   ├─ Content rarely changes (&lt; daily)?\n│   │   └─ SSG — prerender: { routes: ['/', '/about'] }\n│   └─ Content updated frequently?\n│       └─ ISR — routeRules: { '/blog/**': { isr: 3600 } }\n│\n├─ Dynamic per-user content (dashboards, apps)?\n│   └─ SSR — ssr: true (Nuxt default)\n│       Best for SEO + authenticated data\n│\n├─ Admin panel / internal tool (no SEO needed)?\n│   └─ SPA — ssr: false in nuxt.config.ts\n│\n├─ Mixed needs (marketing pages + app)?\n│   └─ Hybrid — routeRules per path\n│       routeRules: {\n│         '/': { prerender: true },\n│         '/blog/**': { isr: 3600 },\n│         '/app/**': { ssr: true },\n│         '/admin/**': { ssr: false },\n│       }\n│\n└─ Deploying to...\n    ├─ Cloudflare Workers/Pages → preset: 'cloudflare'\n    ├─ Vercel → preset: 'vercel' (auto-detected)\n    ├─ Netlify → preset: 'netlify' (auto-detected)\n    └─ Node.js server → preset: 'node-server'\n</code></pre>\n<hr>\n<h2>Rendering Performance Quick Wins</h2>\n<table>\n<thead>\n<tr>\n<th>Technique</th>\n<th>When to Use</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>v-memo=\"[dep1, dep2]\"</code></td>\n<td>Skip re-rendering a subtree (usually a <code>v-for</code> row) unless listed deps changed — only for measured hot lists</td>\n</tr>\n<tr>\n<td><code>&lt;KeepAlive&gt;</code></td>\n<td>Cache component instances across tab/route switches; pair with <code>onActivated</code>/<code>onDeactivated</code> for refresh logic</td>\n</tr>\n<tr>\n<td>Virtual scrolling</td>\n<td>Lists with hundreds+ of rows — <code>vue-virtual-scroller</code> or <code>@tanstack/vue-virtual</code> render only visible items</td>\n</tr>\n<tr>\n<td><code>shallowRef</code> / <code>markRaw</code></td>\n<td>Large objects or third-party instances that don't need deep reactivity (see Reactivity Decision Tree)</td>\n</tr>\n</tbody>\n</table>\n<pre><code>&lt;!-- v-memo: row re-renders only when item.id or selection state changes --&gt;\n&lt;div\n  v-for=\"item in list\"\n  :key=\"item.id\"\n  v-memo=\"[item.id, item.id === selectedId]\"\n&gt;\n  {{ item.name }} — {{ item.id === selectedId ? 'selected' : '' }}\n&lt;/div&gt;\n</code></pre>\n<p><strong>Tip:</strong> before writing a composable, check <a href=\"https://vueuse.org/\">VueUse</a> — 200+ battle-tested composables (<code>useLocalStorage</code>, <code>useIntersectionObserver</code>, <code>useDark</code>, ...) that handle SSR and cleanup edge cases.</p>\n<h2>Common Gotchas</h2>\n<table>\n<thead>\n<tr>\n<th>Gotcha</th>\n<th>Why</th>\n<th>Fix</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Reactivity lost after destructuring <code>reactive()</code></td>\n<td>Destructuring extracts plain values, not refs</td>\n<td>Use <code>toRefs(state)</code> when destructuring, or use <code>ref()</code> instead of <code>reactive()</code></td>\n</tr>\n<tr>\n<td><code>ref.value</code> needed in <code>&lt;script&gt;</code>, not in <code>&lt;template&gt;</code></td>\n<td>Template auto-unwraps top-level refs</td>\n<td>Access as <code>count</code> in template, <code>count.value</code> in script</td>\n</tr>\n<tr>\n<td><code>watch</code> doesn't fire on nested object changes</td>\n<td>Default is shallow watch</td>\n<td>Add <code>{ deep: true }</code> or watch a specific nested path <code>() =&gt; obj.nested.prop</code></td>\n</tr>\n<tr>\n<td>Async setup breaks SSR in Nuxt</td>\n<td><code>await</code> in <code>setup()</code> suspends the component</td>\n<td>Use <code>useAsyncData</code> or <code>useFetch</code> — never raw <code>await fetch()</code> in Nuxt setup</td>\n</tr>\n<tr>\n<td><code>watchEffect</code> runs immediately and tracks lazily</td>\n<td>Tracks dependencies at runtime, not statically</td>\n<td>Use <code>watch</code> with explicit sources when you need control over what's tracked</td>\n</tr>\n<tr>\n<td>Template refs are <code>null</code> before mount</td>\n<td><code>ref()</code> is null until component is mounted</td>\n<td>Access template refs inside <code>onMounted</code> or use <code>watch</code> with <code>{ immediate: false }</code></td>\n</tr>\n<tr>\n<td>Pinia store state lost when destructuring</td>\n<td>State properties are not reactive when pulled out directly</td>\n<td>Always use <code>storeToRefs(store)</code> for state/getters; destructure actions directly</td>\n</tr>\n<tr>\n<td>Props are readonly — mutating causes warning</td>\n<td>Vue enforces one-way data flow</td>\n<td>Emit event to parent and let parent update; or use <code>defineModel()</code> for two-way binding</td>\n</tr>\n<tr>\n<td><code>computed</code> setter not called on direct assignment</td>\n<td>Computed with no setter is read-only by default</td>\n<td>Define <code>get</code> and <code>set</code>: <code>computed({ get: () =&gt; ..., set: (v) =&gt; ... })</code></td>\n</tr>\n<tr>\n<td><code>v-model</code> on component uses wrong prop/event name</td>\n<td>Default v-model uses <code>modelValue</code> prop and <code>update:modelValue</code> event</td>\n<td>Use <code>defineModel()</code> (Vue 3.4+) or manually wire <code>modelValue</code> prop + <code>update:modelValue</code> emit</td>\n</tr>\n<tr>\n<td><code>provide</code> value is not reactive</td>\n<td>Providing a raw value instead of a ref</td>\n<td>Provide <code>ref()</code> or <code>reactive()</code> so injectors see updates: <code>provide('key', ref(value))</code></td>\n</tr>\n<tr>\n<td><code>defineAsyncComponent</code> error not caught</td>\n<td>Async component rejects without error boundary</td>\n<td>Add <code>errorComponent</code> option or wrap in <code>&lt;Suspense&gt;</code> with error slot</td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>Reference Files</h2>\n<table>\n<thead>\n<tr>\n<th>File</th>\n<th>When to Load</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"./references/composition-api.md\">./references/composition-api.md</a></td>\n<td>Composables, provide/inject, template refs, custom directives, Teleport, Suspense, slots, transitions, v-model deep patterns</td>\n</tr>\n<tr>\n<td><a href=\"./references/state-routing.md\">./references/state-routing.md</a></td>\n<td>Pinia advanced patterns (plugins, SSR, store composition), Vue Router (guards, meta typing, scroll behavior, transitions)</td>\n</tr>\n<tr>\n<td><a href=\"./references/nuxt.md\">./references/nuxt.md</a></td>\n<td>Nuxt 4 directory structure, data fetching, server routes, middleware, plugins, modules, SEO, deployment, Nuxt Content</td>\n</tr>\n<tr>\n<td><a href=\"./references/testing.md\">./references/testing.md</a></td>\n<td>Vitest setup, Vue Test Utils, Pinia/Router testing, composable testing, MSW, Playwright, Nuxt test utils</td>\n</tr>\n</tbody>\n</table>\n<h2>Staleness Verifier</h2>\n<p>This skill encodes fast-moving facts (the Vue 3 minor-version gates, the Nuxt 4\nmeta-framework, the ecosystem package stack). <a href=\"scripts/check-vue-facts.py\"><code>scripts/check-vue-facts.py</code></a>\nguards them against silent drift — internal consistency in PR CI, live\nmajor-version drift in the scheduled freshness job:</p>\n<pre><code># Structural (PR CI, no network): every catalogued package + Vue version gate is\n# still named in this skill's prose, and the currency note still carries a year.\npython3 skills/vue-ops/scripts/check-vue-facts.py --offline        # exit 0 consistent, 10 drift\n\n# Live (weekly freshness job, never blocks a PR): is any documented major\n# now behind npm's latest dist-tag? (e.g. Nuxt 5 while the prose says Nuxt 4.)\npython3 skills/vue-ops/scripts/check-vue-facts.py --live           # exit 10 a major moved ahead, 7 npm unreachable\n</code></pre>\n<p>The canonical fact list lives in <a href=\"assets/vue-facts.json\"><code>assets/vue-facts.json</code></a>; when you add or drop a recommendation or the prose stops naming one, update it to match or <code>--offline</code> fails CI.</p>\n<hr>\n<h2>See Also</h2>\n<ul>\n<li><strong>typescript-ops</strong> — TypeScript generics, utility types, strict mode configuration</li>\n<li><strong>testing-ops</strong> — General testing patterns, TDD, mocking strategies, CI integration</li>\n<li><strong>tailwind-ops</strong> — Tailwind CSS with Vue component patterns, dark mode, responsive design</li>\n<li><strong>javascript-ops</strong> — Modern JS patterns used alongside Vue (async/await, modules, iterators)</li>\n</ul>\n","files":[{"path":"assets/.gitkeep","sizeBytes":0,"isText":false},{"path":"assets/vue-facts.json","sizeBytes":1764,"isText":true},{"path":"references/composition-api.md","sizeBytes":18560,"isText":true},{"path":"references/nuxt.md","sizeBytes":20638,"isText":true},{"path":"references/state-routing.md","sizeBytes":16550,"isText":true},{"path":"references/testing.md","sizeBytes":21271,"isText":true},{"path":"scripts/check-vue-facts.py","sizeBytes":11298,"isText":true},{"path":"scripts/.gitkeep","sizeBytes":0,"isText":false},{"path":"SKILL.md","sizeBytes":15661,"isText":true},{"path":"tests/run.sh","sizeBytes":3572,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-30T19:39:31.256298Z","sha256":"C48FE5166B903E85B38E2CF6AC0FF1A7C93C09B19DA0DBA251ACA797C807FACB","sizeBytes":38747},"review":null,"source":{"repositoryUrl":"https://github.com/0xDarkMatter/claude-mods","path":"skills/vue-ops","license":"MIT","commit":"3dfaf0ba5753026a99ee13f9d9ed56b9793bb6e8","subtreeSha":"1C2C119875F06723ABE0BA5FEB0A39DFD79CD4EDA96FBCA970447363D05490A6","lastSyncedAt":"2026-09-30T19:37:28.226022Z"},"reviewedAt":"2026-09-30T19:44:17.573829Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/vue-ops"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart"},{"target":"git","command":"git clone https://github.com/0xDarkMatter/claude-mods.git"}]}