Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Vue.js 3 Composition API patterns, component architecture, reactivity best practices, Pinia state management, Vue Router navigation, and Nuxt SSR patterns. Activates for Vue, Nuxt, Vite, or Pinia projects.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 331% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 191% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 205% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 202% | 0% |
Comprehensive guide for Vue.js 3 development using Composition API (<script setup>), covering component design, reactivity, state management, routing, testing, and SSR patterns. Nuxt-specific guidance is included where it differs from vanilla Vue.
Activate this skill when:
.vue files).src/
├── api/ # API client and endpoint definitions
├── assets/ # Static assets (images, fonts, icons)
├── components/ # Shared/reusable components
│ ├── base/ # Base UI primitives (Button, Input, Modal)
│ └── features/ # Feature-specific shared components
├── composables/ # Reusable Composition API logic
├── layouts/ # Page layouts (optional)
├── pages/ # Route-level page components
├── router/ # Vue Router configuration
├── stores/ # Pinia stores
├── types/ # TypeScript type definitions
├── utils/ # Pure utility functions
└── App.vue # Root component| Convention | When to Use | |-----------|-------------| | PascalCase.vue | All components (enforced by vue/multi-word-component-names) | | useCamelCase.ts | Composables | | camelCase.ts | Utilities, API clients, types | | kebab-case directories | Route segments, feature folders |
vue<script setup lang="ts"> // 1. Imports (vue → ecosystem → absolute → relative) // 2. Props & Emits & Slots // 3. Composables // 4. Local state (ref/reactive) // 5. Computed properties // 6. Methods // 7. Watchers // 8. Lifecycle hooks </script> <template> <!-- Template content --> </template> <style scoped> /* Scoped styles */ </style>
ts// Type-based props with defaults interface Props { label: string; variant?: "primary" | "secondary"; disabled?: boolean; items: Item[]; } const props = withDefaults(defineProps<Props>(), { variant: "primary", disabled: false, });
type, and required/default where appropriate.isXxx, hasXxx, canXxx.defineModel() (Vue 3.4+) or modelValue + update:modelValue.tsconst emit = defineEmits<{ submit: []; "update:modelValue": [value: string]; select: [id: string, index: number]; }>();
@update:model-value).emit("update:modelValue", val)).ts// composables/useDebounce.ts export function useDebounce<T>(value: MaybeRef<T>, delay: number): Ref<T> { const debounced = ref(toValue(value)) as Ref<T>; let timer: ReturnType<typeof setTimeout>; watch( () => toValue(value), (newVal) => { clearTimeout(timer); timer = setTimeout(() => { debounced.value = newVal; }, delay); } ); onUnmounted(() => clearTimeout(timer)); return readonly(debounced); }
use prefix.ref, computed, reactive), never plain primitives.MaybeRef / toRef() / toValue().onUnmounted or watcher onCleanup.Composables replace Vue 2 mixins entirely:
| Pattern | Use Case | |---------|----------| | ref() / reactive() | Local component state | | Props + Emits | Parent-child communication | | Provide / Inject | Theme, config, plugin API | | Pinia store | Global, shared, complex state | | Server state composable | API data with caching (wrap fetch/TanStack Query) |
ts// stores/useCartStore.ts export const useCartStore = defineStore("cart", () => { const items = ref<CartItem[]>([]); const isLoading = ref(false); const totalPrice = computed(() => items.value.reduce((sum, i) => sum + i.price * i.quantity, 0) ); const itemCount = computed(() => items.value.reduce((sum, i) => sum + i.quantity, 0) ); async function addItem(productId: string) { isLoading.value = true; try { const item = await fetchProduct(productId); const existing = items.value.find(i => i.id === item.id); if (existing) existing.quantity++; else items.value.push({ ...item, quantity: 1 }); } finally { isLoading.value = false; } } return { items, isLoading, totalPrice, itemCount, addItem }; });
$patch() for grouped updates.tsconst routes = [ { path: "/users/:id", name: "user-detail", component: () => import("@/pages/UserDetail.vue"), // lazy props: true, // pass params as props meta: { requiresAuth: true }, }, ];
tsrouter.beforeEach((to, from) => { const { isLoggedIn } = useAuthStore(); if (to.meta.requiresAuth && !isLoggedIn) { return { name: "login", query: { redirect: to.fullPath } }; } });
When a component stays mounted but route params change:
tsconst route = useRoute(); const id = computed(() => route.params.id as string); watch(id, (newId) => fetchItem(newId));
vue<!-- v-if/v-else-if/v-else --> <div v-if="isLoading">Loading...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else>{{ content }}</div> <!-- v-show for frequent toggles --> <div v-show="isOpen">Toggled content</div> <!-- v-for with stable keys --> <div v-for="item in items" :key="item.id">{{ item.name }}</div> <!-- Computed filtered list (not v-if + v-for on same element) --> <div v-for="item in activeItems" :key="item.id">{{ item.name }}</div> <!-- Event handling --> <form @submit.prevent="handleSubmit"> <button type="submit">Save</button> </form> <!-- v-model --> <input v-model="name" /> <CustomInput v-model="value" v-model:title="title" />
| Technique | When to Use | |-----------|-------------| | v-memo | List items that rarely change | | v-once | Content rendered once and static forever | | shallowRef() | Large data structures replaced wholesale | | shallowReactive() | Only top-level properties are reactive | | v-show over v-if | Frequent visibility toggles | | <KeepAlive :max="10"> | Cache toggled views | | Lazy routes | () => import(...) for non-critical routes | | Suspense | Async component loading with fallback |
tsimport { mount } from "@vue/test-utils"; import { createPinia, setActivePinia } from "pinia"; import UserCard from "./UserCard.vue"; beforeEach(() => { setActivePinia(createPinia()); }); it("renders and emits", async () => { const wrapper = mount(UserCard, { props: { user: { id: "1", name: "Alice" } }, }); expect(wrapper.text()).toContain("Alice"); await wrapper.find("button").trigger("click"); expect(wrapper.emitted("select")![0]).toEqual(["1"]); });
Nuxt auto-imports ref, computed, watch, useFetch, useAsyncData, etc. Use them directly without importing. For non-Nuxt projects, always import explicitly.
tsconst { data: user, pending, error, refresh } = await useAsyncData( "user", // unique key for caching () => $fetch(`/api/users/${id}`), ); const { data: posts } = await useFetch("/api/posts", { query: { page: 1 }, key: "posts-page-1", // dedupes requests });
ts// server/api/users/[id].ts export default defineEventHandler(async (event) => { const { id } = await getValidatedRouterParams(event, z.object({ id: z.string().uuid(), }).parse); // ... fetch and return });
ts// nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { // server-only apiSecret: "", // public (exposed to client) public: { apiBase: "https://api.example.com", }, }, });
Vue 3.5 stabilized reactive props destructure — destructured variables from defineProps() are automatically reactive:
ts// Vue 3.5+: destructured props are reactive (no need for toRefs) const { count = 0, msg = "hello" } = defineProps<{ count?: number; msg?: string; }>(); // Limitation: cannot watch destructured prop directly watch(() => count, (newVal) => { ... }); // PASS getter required
useTemplateRef()Replace name-matched plain refs with useTemplateRef() for template references:
tsimport { useTemplateRef } from "vue"; const inputEl = useTemplateRef<HTMLInputElement>("input"); // "input" matches the ref="input" attribute in template, not the variable name
Supports dynamic ref IDs: useTemplateRef(dynamicRefId).
onWatcherCleanup()Globally importable watcher cleanup API (Vue 3.5+). It must be called synchronously inside the watcher callback:
tsimport { watch, onWatcherCleanup } from "vue"; watch(userId, async (newId) => { const controller = new AbortController(); onWatcherCleanup(() => controller.abort()); // ... fetch with signal });
useId()SSR-stable unique ID generation for form elements and accessibility:
tsimport { useId } from "vue"; const id = useId();
defer Teleport<Teleport defer> allows teleporting to targets rendered in the same cycle:
vue<Teleport defer to="#container">Content</Teleport> <div id="container"></div>
defineAsyncComponent() now supports hydrate strategy:
tsimport { defineAsyncComponent, hydrateOnVisible } from "vue"; const AsyncComp = defineAsyncComponent({ loader: () => import("./Comp.vue"), hydrate: hydrateOnVisible(), });
| Anti-Pattern | Why It's Wrong | The Fix | |-------------|---------------|---------| | Destructuring defineProps() (Vue < 3.5) | Captures snapshot, loses reactivity | Access via props.xxx or use toRefs() | | watch() on destructured prop (Vue 3.5+) | Compile-time error — destructured props can't be watched directly | Use getter wrapper: watch(() => count, ...) | | v-if + v-for on same element | Ambiguous execution order | Use computed filtered array | | v-for key = index | Broken state on reorder | Use stable database IDs | | Mutating props | Violates one-way data flow | Emit events or use v-model | | v-html with user content | XSS vulnerability | Sanitize with DOMPurify | | Mixins in Vue 3 | Opaque, collision-prone | Replace with composables | | Module-scope side effects in composable | Shared across instances | Scope in onMounted + onUnmounted | | reactive() for replaceable state | Replacement breaks reactivity | Use ref() instead | | Watcher without cleanup | Memory leaks, race conditions | Use onCleanup or onWatcherCleanup() (Vue 3.5+) | | Options API in new Vue 3 code | Ecosystem move to Composition API | Use <script setup> | | Plain ref for template references | No dynamic ref support, name-matching fragile | Use useTemplateRef() (Vue 3.5+) |
accessibility — ARIA, semantic HTML, focus managementfrontend-patterns — Cross-framework frontend architecturetypescript — TypeScript best practices applied to Vue projectscoding-standards — General code quality standardsOther measured skills in the registry, with their headline benchmark lift.