vue-patterns
affaan-m/ECC
Vue.js 3のコンポジションAPIのパターン、コンポーネントアーキテクチャ、リアクティブプログラミングのベストプラクティス、Piniaによる状態管理、Vue Routerによるナビゲーション、およびVue、Nuxt、Vite、Piniaプロジェクト向けのNuxt SSRパターンを提供します。
...すべて拡張しますVue.js のパターンとベストプラクティス
Composition API を使用した Vue.js 3 開発のための包括的なガイド()を用いたVue.js 3開発のための包括的なガイド。コンポーネント設計、リアクティブ性、状態管理、ルーティング、テスト、SSRパターンを網羅しています。Nuxt固有のガイドラインについては、vanilla Vueとの違いがある場合に記載されています。
有効にするタイミング
以下の場合にこのスキルを有効にしてください:
- プロジェクトで Vue.js(どのバージョンでも)、Nuxt、Vite + Vue、または Pinia を使用している場合。
- ユーザーがVueのコンポーネントアーキテクチャ、コンポーザブル、リアクティブ性、または状態管理について質問した場合。
- Vueのシングルファイルコンポーネント(
.vueファイル)を確認している場合。 - Vue Router、Piniaのストア、またはVite/Vitestの設定を行っている場合。
- Vue特有のパフォーマンス、セキュリティ、またはSSRパターンについて議論している場合。
1. プロジェクト構造
推奨レイアウト(機能優先)
src/
├── api/ # API クライアントおよびエンドポイントの定義
├── assets/ # 静的アセット(画像、フォント、アイコン)
├── components/ # 共有・再利用可能なコンポーネント
│ ├── base/ # 基本的な UI プリミティブ(Button、Input、Modal)
│ └── features/ # 機能固有の共有コンポーネント
├── composables/ # 再利用可能な Composition API ロジック
├── layouts/ # ページレイアウト(オプション)
├── pages/ # ルートレベルのページコンポーネント
├── router/ # Vue Routerの設定
├── stores/ # Piniaのストア
├── types/ # TypeScriptの型定義
├── utils/ # ピュアなユーティリティ関数
└── App.vue # ルートコンポーネント
ファイル名の命名規則
| 規約 | 使用する場合 |
|---|---|
PascalCase.vue |
すべてのコンポーネント(vue/multi-word-component-names によって強制される) |
useCamelCase.ts |
コンポーザブル |
camelCase.ts |
ユーティリティ、APIクライアント、型 |
ケバブ式ディレクトリ |
ルートセグメント、機能フォルダ |
2. コンポーネントアーキテクチャ
単一ファイルコンポーネントの順序
プレゼンテーションコンポーネントとコンテナコンポーネント
- コンテナコンポーネント:データ取得、状態、および副作用を独自に管理する。プレゼンテーションコンポーネントをレンダリングする。
- プレゼンテーションコンポーネント:プロップを受け取り、イベントを発行する。API呼び出しやストアへのアクセスは行わない。純粋なレンダリングを行う。
プロップスのベストプラクティス
// デフォルト値を持つ型ベースのプロップス
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps(), {
variant: "primary",
disabled: false,
});
- 常に
型を指定し、必要に応じて必須条件やデフォルト値を指定してください。 - ブール型プロパティ:
isXxx、hasXxx、canXxx。 - プロパティを直接変更してはいけません。代わりにイベントを発火させてください。
- v-model バインディングには、
defineModel()(Vue 3.4 以降)またはmodelValue+update:modelValueを使用してください。
イベント
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- テンプレート内ではケバブケースを使用してください (
@update:model-value)。 - スクリプト内ではキャメルケースを使用してください(
emit("update:modelValue", val))。
3. コンポーザブル(再利用可能なロジック)
構造
// 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);
}
ルール
- 必ず
`use` プレフィックスで始めること。 - リアクティブな値(
ref、computed、reactive)を返すこと。単純なプリミティブ型は決して返してはならない。 MaybeRef/toRef()/toValue()を通じてリアクティブな入力を受け入れること。onUnmountedまたはウォッチャーのonCleanup内で副作用をクリーンアップすること。- モジュールスコープでの副作用は許されない。
vs ミックスイン
Composables は Vue 2 のミックスインを完全に置き換えます:
- Mixins:不透明なデータフロー、真実の源の競合、名前衝突。
- コンポーザブル: 明示的なインポート、明確な戻り値、コンポーザブルかつツリーシェイク可能。
4. 状態管理
いつ何をすべきか
| パターン | ユースケース |
|---|---|
ref()/reactive() |
ローカルコンポーネントの状態 |
| プロパティ + エミット | 親子間の通信 |
| 提供 / 注入 | テーマ、設定、プラグイン API |
| Pinia ストア | グローバル、共有、複雑な状態 |
| サーバー状態のコンポーザブル化 | キャッシュ付きAPIデータ(fetch/TanStack Queryのラップ) |
Pinia セットアップストア(推奨)
// 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 };
});
- Setup Storeの構文を使用してください(Options Storeではありません)。
- ビジネスレベルの変更にはアクションを、グループ化された更新には
$patch()を優先して使用してください。 - すべての非同期アクションにおいて、ローディング・成功・エラーの処理を行う。
5. Vue Router
ルーティング定義
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // 遅延読み込み
props: true, // パラメータをプロパティとして渡す
meta: { requiresAuth: true },
},
];
ナビゲーションガード
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
リアクティブなルートパラメータ
コンポーネントがマウントされたまま、ルートパラメータが変更された場合:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. テンプレートパターン
テンプレート構文
読み込み中...
エラー: {{ error }}
{{ content }}
切り替え可能なコンテンツ
{{ item.name }}
{{ item.name }}
7. パフォーマンス
| テクニック | 使用場面 |
|---|---|
v-memo |
めったに変更されないリスト項目 |
v-once |
一度レンダリングされ、その後ずっと静的なコンテンツ |
shallowRef() |
大規模なデータ構造を一括で置換する |
shallowReactive() |
トップレベルのプロパティのみがリアクティブ |
v-ifではなくv-show |
頻繁な表示状態の切り替え |
|
表示状態が切り替わるビューをキャッシュ |
| 遅延読み込みルート | 重要度の低いルートには`() => import(...)` を使用 |
Suspense |
フォールバック付きのアシンクロナスコンポーネント読み込み |
8. テスト
Stack
- ユニットテストおよびコンポーネントテストのためのVitest
- マウントおよびインタラクションのためのVue Test Utils
- ストアのモック化には@pinia/testingを使用
- E2EテストにはPlaywright
コンポーネントテストパターン
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("レンダリングされ、イベントを発行する", 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固有のパターン
自動インポート
Nuxt では、ref、computed、watch、useFetch、useAsyncData などが自動インポートされます。インポートせずに直接使用できます。Nuxt 以外のプロジェクトでは、常に明示的にインポートしてください。
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // キャッシュ用の一意なキー
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // リクエストの重複を防止
});
サーバーのルーティング
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... データを取得して返す
});
ランタイム設定
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// サーバー専用
apiSecret: "",
// 公開用(クライアントに公開)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Vue 3.5+ の新API
リアクティブプロップのデストラクチャリング
Vue 3.5 では、リアクティブなプロップのデストラクチャが安定化されました。defineProps()からデストラクチャされた変数は自動的にリアクティブになります:
// Vue 3.5+: デストラクトされたプロパティはリアクティブ(toRefs は不要)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// 制限事項: デストラクチャされたプロパティを直接監視することはできません
watch(() => count, (newVal) => { ... }); // 合格 (getter が必要)
useTemplateRef()
テンプレート参照については、名前が一致する通常の ref をuseTemplateRef()に置き換えてください:
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef("input");
// "input" はテンプレート内の ref="input" 属性と一致し、変数名とは一致しません
動的な ref ID をサポートしています:useTemplateRef(dynamicRefId)。
onWatcherCleanup()
グローバルにインポート可能なウォッチャーのクリーンアップ API(Vue 3.5 以降)。ウォッチャーのコールバック内部で同期的に呼び出す必要があります:
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... シグナルを使用してフェッチ
});
useId()
フォーム要素およびアクセシビリティ向けの、SSR対応の安定した一意のID生成:
import { useId } from "vue";
const id = useId();
deferTeleport
同じレンダリングサイクル内でレンダリングされたターゲットへのテレポートを可能にします:
コンテンツ
遅延ハイドレーション (SSR)
defineAsyncComponent() がハイドレーション戦略をサポートするようになりました:
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
アンチパターン
| アンチパターン | なぜ間違っているのか | 修正方法 |
|---|---|---|
defineProps()のデストラクチャリング (Vue 3.5 以前) |
スナップショットを取得し、リアクティブ性を失う | props.xxx経由でアクセスするか、toRefs()を使用する |
デストラクチャリングされたプロパティにwatch() を適用(Vue 3.5 以降) |
コンパイル時エラー — デストラクチャリングされたプロパティは直接監視できない | ゲッターラッパーを使用:watch(() => count, ...) |
同一要素でのv-ifとv-forの併用 |
実行順序が曖昧 | 計算されたフィルタリング済み配列を使用する |
v-for のkey = index |
順序変更時の状態の破損 | 安定したデータベースIDを使用する |
| プロパティの変更 | 一方向データフローに違反している | イベントを発行するか、v-modelを使用する |
ユーザーコンテンツを含むv-html |
XSSの脆弱性 | DOMPurify によるサニタイズ |
| Vue 3 におけるミックスイン | 不透明で、競合が生じやすい | コンポーザブルへの置き換え |
| コンポーザブル内のモジュールスコープの副作用 | インスタンス間で共有される | onMountedおよびonUnmountedにおけるスコープ |
置換可能な状態には `reactive()` を使用 |
置換によりリアクティブ性が損なわれる | 代わりにref()を使用する |
| クリーンアップのないウォッチャー | メモリリーク、競合状態 | onCleanupまたはonWatcherCleanup()を使用してください(Vue 3.5 以降) |
| 新しい Vue 3 コードにおける Options API | エコシステムが Composition API へ移行 | 使用方法 |
| テンプレート参照にはプレーン ref を使用 | 動的な ref のサポートなし、名前の一致による依存関係が不安定 | useTemplateRef()を使用する (Vue 3.5 以降) |
関連スキル
アクセシビリティ— ARIA、セマンティックHTML、フォーカス管理フロントエンドパターン— クロスフレームワークのフロントエンドアーキテクチャTypeScript— Vueプロジェクトに適用されるTypeScriptのベストプラクティスコーディング標準— 一般的なコード品質基準
---
name: vue-patterns
description: Provides Vue.js 3 Composition API patterns, component architecture, reactivity best practices, Pinia state management, Vue Router navigation, and Nuxt SSR patterns for Vue, Nuxt, Vite, or Pinia projects.
---
# Vue.js Patterns and Best Practices
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.
## When to Activate
Activate this skill when:
- The project uses Vue.js (any version), Nuxt, Vite + Vue, or Pinia.
- The user asks about Vue component architecture, composables, reactivity, or state management.
- Reviewing Vue Single-File Components (`.vue` files).
- Setting up Vue Router, Pinia stores, or Vite/Vitest configuration.
- Discussing Vue-specific performance, security, or SSR patterns.
---
## 1. Project Structure
### Recommended Layout (Feature-First)
```
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
```
### File Naming
| 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 |
---
## 2. Component Architecture
### Single-File Component Order
```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>
```
### 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<Props>(), {
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<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);
}
```
### 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<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 };
});
```
- Use Setup Store syntax (not Options Store).
- Prefer actions for business-level mutations and `$patch()` for grouped updates.
- 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
<!-- 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" />
```
---
## 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 |
| `<KeepAlive :max="10">` | 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) => { ... }); // PASS getter required
```
### `useTemplateRef()`
Replace name-matched plain refs with `useTemplateRef()` for template references:
```ts
import { 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:
```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
`<Teleport defer>` allows teleporting to targets rendered in the same cycle:
```vue
<Teleport defer to="#container">Content</Teleport>
<div id="container"></div>
```
### 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 `<script setup>` |
| Plain ref for template references | No dynamic ref support, name-matching fragile | Use `useTemplateRef()` (Vue 3.5+) |
## Related Skills
- `accessibility` — ARIA, semantic HTML, focus management
- `frontend-patterns` — Cross-framework frontend architecture
- `typescript` — TypeScript best practices applied to Vue projects
- `coding-standards` — General code quality standards
すべてのファイル
1件のファイルvue-patternsをインストール
スキルファイルをダウンロードし、.claude/skills/ ディレクトリに解凍してください。
ZIPをダウンロードリポジトリをクローンし、スキルファイルをプロジェクトにコピーしてください。
git clone https://github.com/affaan-m/ECC/tree/main/skills/vue-patterns # Copy SKILL.md to your .claude/skills/ directory
コピー





家
