From fe2d6656a34f70b90d25e1aaf34515132283a270 Mon Sep 17 00:00:00 2001
From: Bujidao <3317431882@qq.com>
Date: Fri, 12 Jun 2026 17:54:28 +0800
Subject: [PATCH] feat(skills): add vue-patterns skill for Vue.js 3 best
practices
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add skills/vue-patterns/SKILL.md covering:
- Project structure (feature-first layout, file naming)
- Component architecture (SFC order, presentational vs container, props/emits)
- Composables (use prefix, MaybeRef/toValue, cleanup, vs mixins)
- State management decision tree (local → props → provide/inject → Pinia → server state)
- Vue Router patterns (lazy loading, navigation guards, reactive params)
- Template patterns (v-if/v-else, v-show, v-for, v-model with defineModel)
- Performance techniques (shallowRef, v-memo, v-once, KeepAlive, Suspense)
- Testing stack and patterns (Vitest, Vue Test Utils, Pinia testing)
- Nuxt-specific patterns (auto-imports, useAsyncData, server routes, runtime config)
- Vue 3.5+ new APIs section: reactive props destructure, useTemplateRef,
onWatcherCleanup, useId, defer Teleport, lazy hydration
- Anti-patterns table with Vue 3.5+ version-specific notes
---
skills/vue-patterns/SKILL.md | 471 +++++++++++++++++++++++++++++++++++
1 file changed, 471 insertions(+)
create mode 100644 skills/vue-patterns/SKILL.md
diff --git a/skills/vue-patterns/SKILL.md b/skills/vue-patterns/SKILL.md
new file mode 100644
index 00000000..1312de03
--- /dev/null
+++ b/skills/vue-patterns/SKILL.md
@@ -0,0 +1,471 @@
+---
+name: vue-patterns
+description: 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.
+origin: ECC
+---
+
+# Vue.js Patterns and Best Practices
+
+Comprehensive guide for Vue.js 3 development using Composition API (`
+
+
+
+
+
+
+```
+
+### Presentational vs Container
+
+- **Container components**: Own data fetching, state, and side effects. Render presentational components.
+- **Presentational components**: Receive props, emit events. No API calls, no store access. Pure rendering.
+
+### Props Best Practices
+
+```ts
+// Type-based props with defaults
+interface Props {
+ label: string;
+ variant?: "primary" | "secondary";
+ disabled?: boolean;
+ items: Item[];
+}
+
+const props = withDefaults(defineProps(), {
+ variant: "primary",
+ disabled: false,
+});
+```
+
+- Always provide `type`, and `required`/`default` where appropriate.
+- Boolean props: `isXxx`, `hasXxx`, `canXxx`.
+- Never mutate props — emit events instead.
+- For v-model binding, use `defineModel()` (Vue 3.4+) or `modelValue` + `update:modelValue`.
+
+### Events
+
+```ts
+const emit = defineEmits<{
+ submit: [];
+ "update:modelValue": [value: string];
+ select: [id: string, index: number];
+}>();
+```
+
+- Use kebab-case in templates (`@update:model-value`).
+- Use camelCase in script (`emit("update:modelValue", val)`).
+
+---
+
+## 3. Composables (Reusable Logic)
+
+### Structure
+
+```ts
+// composables/useDebounce.ts
+export function useDebounce(value: MaybeRef, delay: number): Ref {
+ const debounced = ref(toValue(value)) as Ref;
+
+ let timer: ReturnType;
+ watch(
+ () => toValue(value),
+ (newVal) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => { debounced.value = newVal; }, delay);
+ }
+ );
+
+ onUnmounted(() => clearTimeout(timer));
+ return readonly(debounced);
+}
+```
+
+### Rules
+
+- Must start with `use` prefix.
+- Return reactive values (`ref`, `computed`, `reactive`), never plain primitives.
+- Accept reactive inputs via `MaybeRef` / `toRef()` / `toValue()`.
+- Clean up side effects in `onUnmounted` or watcher `onCleanup`.
+- No module-scope side effects.
+
+### vs Mixins
+
+Composables replace Vue 2 mixins entirely:
+- **Mixins**: Opaque data flow, source-of-truth collisions, name conflicts.
+- **Composables**: Explicit imports, clear return values, composable and tree-shakable.
+
+---
+
+## 4. State Management
+
+### When to Use What
+
+| 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) |
+
+### Pinia Setup Store (Preferred)
+
+```ts
+// stores/useCartStore.ts
+export const useCartStore = defineStore("cart", () => {
+ const items = ref([]);
+ 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 };
+});
+```
+
+- Use Setup Store syntax (not Options Store).
+- Actions: only place to mutate state.
+- Every async action: handle loading + success + error.
+
+---
+
+## 5. Vue Router
+
+### Route Definitions
+
+```ts
+const routes = [
+ {
+ path: "/users/:id",
+ name: "user-detail",
+ component: () => import("@/pages/UserDetail.vue"), // lazy
+ props: true, // pass params as props
+ meta: { requiresAuth: true },
+ },
+];
+```
+
+### Navigation Guards
+
+```ts
+router.beforeEach((to, from) => {
+ const { isLoggedIn } = useAuthStore();
+ if (to.meta.requiresAuth && !isLoggedIn) {
+ return { name: "login", query: { redirect: to.fullPath } };
+ }
+});
+```
+
+### Reactive Route Params
+
+When a component stays mounted but route params change:
+
+```ts
+const route = useRoute();
+const id = computed(() => route.params.id as string);
+watch(id, (newId) => fetchItem(newId));
+```
+
+---
+
+## 6. Template Patterns
+
+### Template Syntax
+
+```vue
+
+Loading...
+Error: {{ error }}
+{{ content }}
+
+
+Toggled content
+
+
+{{ item.name }}
+
+
+{{ item.name }}
+
+
+
+
+
+
+
+```
+
+---
+
+## 7. Performance
+
+| 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 |
+| `` | Cache toggled views |
+| Lazy routes | `() => import(...)` for non-critical routes |
+| `Suspense` | Async component loading with fallback |
+
+---
+
+## 8. Testing
+
+### Stack
+
+- **Vitest** for unit and component tests
+- **Vue Test Utils** for mounting and interaction
+- **@pinia/testing** for store mocking
+- **Playwright** for E2E
+
+### Component Test Pattern
+
+```ts
+import { 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"]);
+});
+```
+
+---
+
+## 9. Nuxt-Specific Patterns
+
+### Auto-Imports
+
+Nuxt auto-imports `ref`, `computed`, `watch`, `useFetch`, `useAsyncData`, etc. Use them directly without importing. For non-Nuxt projects, always import explicitly.
+
+### useAsyncData / useFetch
+
+```ts
+const { 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
+});
+```
+
+### Server Routes
+
+```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
+});
+```
+
+### Runtime Config
+
+```ts
+// nuxt.config.ts
+export default defineNuxtConfig({
+ runtimeConfig: {
+ // server-only
+ apiSecret: "",
+ // public (exposed to client)
+ public: {
+ apiBase: "https://api.example.com",
+ },
+ },
+});
+```
+
+---
+
+## 10. Vue 3.5+ New APIs
+
+### Reactive Props Destructure
+
+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) => { ... }); // ✅ getter required
+```
+
+### `useTemplateRef()`
+
+Replace name-matched plain refs with `useTemplateRef()` for template references:
+
+```ts
+import { useTemplateRef } from "vue";
+const inputEl = useTemplateRef("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+):
+
+```ts
+import { 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:
+
+```ts
+import { useId } from "vue";
+const id = useId();
+```
+
+### `defer` Teleport
+
+`` allows teleporting to targets rendered in the same cycle:
+
+```vue
+Content
+
+```
+
+### Lazy Hydration (SSR)
+
+`defineAsyncComponent()` now supports `hydrate` strategy:
+
+```ts
+import { defineAsyncComponent, hydrateOnVisible } from "vue";
+const AsyncComp = defineAsyncComponent({
+ loader: () => import("./Comp.vue"),
+ hydrate: hydrateOnVisible(),
+});
+```
+
+---
+
+## Anti-Patterns
+
+| 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 `