オプション
家家 Skill ウェブ開発 vue-patterns

vue-patterns

affaan-m/ECC affaan-m/ECC

Vue.js 3のコンポジションAPIのパターン、コンポーネントアーキテクチャ、リアクティブプログラミングのベストプラクティス、Piniaによる状態管理、Vue Routerによるナビゲーション、およびVue、Nuxt、Vite、Piniaプロジェクト向けのNuxt SSRパターンを提供します。

...すべて拡張します
0
更新された時間 2026年10月2日

Vue.js のパターンとベストプラクティス

Composition API を使用した Vue.js 3 開発のための包括的なガイド(

プレゼンテーションコンポーネントとコンテナコンポーネント

  • コンテナコンポーネント:データ取得、状態、および副作用を独自に管理する。プレゼンテーションコンポーネントをレンダリングする。
  • プレゼンテーションコンポーネント:プロップを受け取り、イベントを発行する。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 へ移行 使用方法

すべてのファイル

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

コピー コピー
クイックセットアップ: スキルフォルダを .claude/skills/ にコピーしてください。 Claude が自動的にそのスキルを検出して使用します。
リポジトリ affaan-m/ECC

関連スキル

github-code-search
更新された時間 2026年6月29日
drizzle-orm
更新された時間 2026年6月29日
clickhouse-io
更新された時間 2026年6月29日
prisma-client-api
更新された時間 2026年6月29日