Vueコンポーネントとコンポーザブルの設計パターン
Jozo
Jozo
2025/03/18
12 min read

Vueコンポーネントとコンポーザブルの設計パターン:スタートアップガイド

このガイドは、スタートアップの開発者向けにカスタマイズされたVueコンポーネントとコンポーザブル設計パターンの実践的な概要を提供します。さまざまなソースからの洞察を活用して、クリーナー、より保守可能で、スケーラブルなVueアプリケーションを書くのに役立ちます。

コンポーネント設計パターン

1. コンポーネントパターン

既存のコンポーネントから再利用可能なコンポーネントを抽出すると、コードが簡潔になり、再利用性が向上します。これは単一責任原則を促進し、コードベースがより モジュラーで保守しやすくなります。

ヒント: 既存コード内の隠されたコンポーネントを特定して抽出してください。繰り返されるUI要素またはカプセル化できるロジックを探してください。

例:

<!-- 前:複雑なフォーム -->
<template>
  <div>
    <label for="name">名前:</label>
    <input type="text" id="name" v-model="name">
    <label for="email">メール:</label>
    <input type="email" id="email" v-model="email">
    <button @click="submitForm">送信</button>
  </div>
</template>
<!-- 後:再利用可能なコンポーネントを使用 -->
<template>
  <div>
    <InputField label="名前" v-model="name" type="text" />
    <InputField label="メール" v-model="email" type="email" />
    <SubmitButton @click="submitForm" />
  </div>
</template>

2. クリーンなコンポーネント

単に機能するだけでなく、コード可読性、保守性、およびテスト可能性を考慮して、良好に機能するコンポーネントを目指してください。クリーンなコンポーネントは理解、修正、および検出が簡単です。

ヒント: 理解しやすく、保守しやすいコンポーネントを作成してください。明確な命名規則、一貫した形式化、および明確に定義された責任を使用してください。

例:

<!-- 不正:関心事が混在したコンポーネント -->
<template>
  <div>
    <button @click="handleClick">{{ buttonText }}</button>
    <div v-if="showDetails">{{ details }}</div>
  </div>
</template>
<script setup>
import { ref } from 'vue';

const buttonText = ref('詳細を表示');
const showDetails = ref(false);
const details = ref('');

async function handleClick() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>
<!-- 良い:焦点を絞った責任のコンポーネント -->
<template>
  <div>
    <ShowDetailsButton @click="toggleDetails" :text="buttonText" />
    <DetailsDisplay v-if="showDetails" :details="details" />
  </div>
</template>
<script setup>
import { ref } from 'vue';
import ShowDetailsButton from './ShowDetailsButton.vue';
import DetailsDisplay from './DetailsDisplay.vue';

const showDetails = ref(false);
const details = ref('');
const buttonText = ref('詳細を表示');

async function toggleDetails() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>

3. 1つのファイルで複数のコンポーネント

小さく自己完結したコンポーネントについては、同じファイルに保つことを検討してください。これはプロジェクトのファイル数を減らし、特に密に結合されたコンポーネントに対する開発速度を向上させることができます。

ヒント: 単純なコンポーネント用に不要なファイルを作成しないようにしてください。このアプローチを使用してください。一箇所でのみ使用されるコンポーネント。

例:

<template>
  <div>
    <MyButton @click="handleClick">ここをクリック</MyButton>
  </div>
</template>

<script setup>
import MyButton from './MyButton.vue';

function handleClick() {
  alert('ボタンがクリックされました!');
}
</script>
<template>
  <button @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
defineEmits(['click']);
</script>

4. 制御されたプロップパターン

このパターンにより、親からコンポーネントの内部状態をオーバーライドできます。モーダルの可視性を制御したり、ドロップダウンの選択を制御する必要がある場合に有用です。

ヒント: 外側からコンポーネントの状態を強制する必要があるときにこのパターンを使用してください。コンポーネントに小道具を渡してその内部状態を制御します。

例:

<!-- Modal.vue -->
<template>
  <div v-if="isOpen" class="modal">
    <div class="modal-content">
      <slot></slot>
      <button @click="closeModal">閉じる</button>
    </div>
  </div>
</template>
<script setup>
defineProps({
  isOpen: {
    type: Boolean,
    default: false
  }
});

const emit = defineEmits(['close']);

function closeModal() {
  emit('close');
}
</script>
<!-- 親コンポーネント -->
<template>
  <div>
    <button @click="showModal = true">モーダルを開く</button>
    <Modal :isOpen="showModal" @close="showModal = false">
      <p>モーダルコンテンツ</p>
    </Modal>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

const showModal = ref(false);
</script>

5. コンポーネントメタデータ

他のコンポーネントに追加情報を提供するためにコンポーネントにメタデータを追加します。これは、コンポーネント構成、追加情報を渡すため、またはコンポーネント間の通信を容易にするために使用できます。

ヒント: コンポーネント構成または追加情報を渡すためにメタデータを使用してください。これはツール化に有用でしたり、他のコンポーネントにコンテキストを提供したりできます。

例:

<!-- コンポーネントA -->
<template>
  <div>コンポーネントA</div>
</template>
<script>
export default {
  meta: {
    componentType: 'display'
  }
}
</script>
<!-- コンポーネントB -->
<template>
  <div>コンポーネントB</div>
</template>
<script>
export default {
  meta: {
    componentType: 'formField'
  }
}
</script>

コンポーザブル設計パターン

1. オプションオブジェクトパターン

オブジェクトを使用してパラメーターをコンポーザブルに渡します。これにより柔軟性とスケーラビリティが可能になります。多くのオプションをコンポーザブルに渡す場合、推奨される方法です。

ヒント: このパターンはVueUseで使用され、コンポーザブルの動作を構成する必要がある場合に強く推奨されます。

例:

// useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url, options = {}) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const { method = 'GET', headers = {}, body = null } = options;

  async function fetchData() {
    loading.value = true;
    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : null
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  onMounted(() => {
    fetchData();
  });

  return { data, loading, error, fetchData };
}

// コンポーネント内:
import { useFetch } from './useFetch';
export default {
  setup() {
    const { data, loading, error } = useFetch('/api/data', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { key: 'value' }
    });
    return { data, loading, error };
  }
}

2. インラインコンポーザブル

新しいファイルを作成することを避けるために、コンポーネントファイル内に直接コンポーザブルを作成します。これは、単一コンポーネントに非常に固有で、他の場所で再利用する予定がないコンポーザブルに特に有用です。 小さくコンポーネント固有のロジックにインラインコンポーザブルを使用します。これはコードを関連付けて一緒に保つことができ、コンポーネント構造を簡潔にできます。

ヒント: 他の場所で再利用する必要がない小さくコンポーネント固有のロジックにインラインコンポーザブルを使用します。このアプローチは関連コードを一緒に保つし、コンポーネント構造を簡潔にします。

例:

コンポーネントに以下のコードがあるとしましょう。

<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const useFormattedDate = () => {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}

const { formattedDate } = useFormattedDate()
</script>

今のところ問題ありませんが、他の場所でこの日付形式が必要な場合、あなたは立ち往生しています。以下がリファクタリングされたバージョンです:

// src/composables/useFormattedDate.ts
import { ref, computed } from 'vue'

export function useFormattedDate() {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}
<!-- コンポーネント内 -->
<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { useFormattedDate } from './composables/useFormattedDate'

const { formattedDate } = useFormattedDate()
</script>

現在、useFormattedDateを任意のコンポーネントで再利用できます。問題が解決しました。

3. より良いコンポーザブルをコーディング

ビジネスロジックの小さな部分を簡単に繰り返し再利用できる機能に抽出します。これはコード再利用を促進し、重複を減らし、コードをより保守しやすくします。

ヒント: コンポーザブルを使用してビジネスロジックを整理して再利用します。コンポーザブルをアプリケーションの再利用可能なビルディングブロックと考えてください。

例:

// useLocalStorage.ts
import { ref, watch } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const storedValue = localStorage.getItem(key);
  const value = ref<T>(storedValue !== null ? JSON.parse(storedValue) : defaultValue);

  watch(
    value,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return value;
}

// コンポーネントでの使用:
<script setup>
import { useLocalStorage } from './useLocalStorage';

const theme = useLocalStorage('theme', 'light');
</script>

4. インターフェースから始める

実装する前にコンポーザブルをどのように使用するかを定義します。これは、コンポーザブルの目的、入力、出力を明確にするのに役立つ「設計最初」開発の形式です。

ヒント: コンポーザブルの入力(小道具、オプション)と出力(返された値)を最初に定義します。これはコンポーザブルのAPIに焦点を当てるのに役立ちます。

例:

// 実装前:useCounter.js
// 初期値を受け入れるべき
// カウントと、増加および減少するメソッドを返すべき
// 実装:
import { ref } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  return { count, increment, decrement };
}

5. スコープスロットでロジックを再利用

スコープスロットを使用してコンポーネント間でユニークな方法でロジックを再利用します。スコープスロットにより、親コンポーネントは子に対してデータとロジックを渡し、機能を共有する柔軟な方法を提供します。

ヒント: スコープスロットを使用して、親コンポーネントから子コンポーネントにデータとロジックを渡します。これにより、子コンポーネントは親によって提供されるデータとロジックに基づいてコンテンツをレンダリングできます。

例:

<!-- DataFetcher.vue -->
<template>
  <div>
    <div v-if="loading">読み込み中...</div>
    <div v-else-if="error">エラー:{{ error }}</div>
    <div v-else>
      <slot :data="data" :loading="loading" :error="error"></slot>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
  url: {
    type: String,
    required: true
  }
});

const data = ref(null);
const loading = ref(true);
const error = ref(null);

onMounted(async () => {
  try {
    const response = await fetch(props.url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    data.value = await response.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>
<!-- コンポーネント内での使用 -->
<DataFetcher url="/api/items">
  <template v-slot="{ data, loading, error }">
    <div v-if="loading">項目を読み込み中...</div>
    <div v-else-if="error">エラー:{{ error }}</div>
    <div v-else>
      <ul>
        <li v-for="item in data" :key="item.id">{{ item.name }}</li>
      </ul>
    </div>
  </template>
</DataFetcher>

一般的なヒントとベストプラクティス

1. Ref対Reactive

refreactiveの違いを理解し、使用例に適切な1つを選択してください。refはプリミティブ値に使用され、reactiveはオブジェクトと配列に使用されます。

ヒント: プリミティブ値にrefを使用し、オブジェクトと配列にreactiveを使用します。これはVueの反応性システムに役立ちます。

例:

<script>
import { ref, reactive } from 'vue';

export default {
  setup() {
    const count = ref(0); // プリミティブ値のための参照
    const user = reactive({ name: 'John', age: 30 }); // オブジェクトのための反応
    return { count, user };
  }
}
</script>

2. 有効な状態管理

アプリケーション内で状態を効果的に構造化します。これはデータフローの管理と、アプリケーションの成長に応じてアプリケーションが管理可能なままであることを確認するために重要です。大規模なアプリケーションの場合は、適切な状態管理ライブラリを検討してください。

ヒント: 大規模なアプリケーションに対して、Piniaまたはvuexの状態管理を検討してください。これらのライブラリは集中状態管理を提供し、複雑なアプリケーション状態の処理を容易にします。

例:(説明的—実装は選択した状態管理ライブラリに依存します)

// Piniaの例(概念的)
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    isLoggedIn: false,
    user: null
  }),
  actions: {
    login(userData) {
      this.isLoggedIn = true;
      this.user = userData;
    },
    logout() {
      this.isLoggedIn = false;
      this.user = null;
    }
  }
});

3. 引用符を使用して入れ子になった値を監視

引用符を使用して直接入れ子になった値を監視します。これにより、特定のプロパティ内の変更を観察でき、それらのプロパティが変更されたときに更新をトリガーします。

ヒント: watchオプションで引用符を使用してオブジェクトの入れ子になったプロパティを監視します。これは、変更を監視するためのより効率的で特定の方法です。

例:

<script>
import { ref, watch } from 'vue';

export default {
  setup() {
    const data = ref({ user: { name: 'John' } });

    watch(() => data.value.user.name, (newName) => {
      console.log('名前が変更されました:', newName);
    });

    return { data };
  }
}
</script>

4. 抽出条件パターン

条件付きロジックに基づいてコンポーネントを分割します。これは可読性と保守性を向上させることで、関心を分離しています。コンポーネントをより理解しやすく、テストしやすくします。

ヒント: コンポーネントに複雑な条件付きロジックがある場合は、より小さなコンポーネントに分割することを検討してください。これはより焦点を絞ったおよび管理可能なコンポーネントを作成します。

例:

<!-- 前:複雑な条件付きロジック -->
<template>
  <div>
    <div v-if="isLoading">読み込み中...</div>
    <div v-else-if="error">エラー:{{ error }}</div>
    <div v-else>
      <UserList v-if="users.length > 0" :users="users" />
      <NoUsersMessage v-else />
    </div>
  </div>
</template>
<!-- 後:抽出されたコンポーネントを使用 -->
<template>
  <LoadingIndicator v-if="isLoading" />
  <ErrorMessage v-if="error" :message="error" />
  <UsersDisplay v-else :users="users" />
</template>

5. コンポーネントを分割する6つの理由

コンポーネント構成と再利用性を向上させるため、より小さい部分にコンポーネントを分割します。これはコードの可読性、保守性、およびテスト可能性を向上させます。

ヒント: より小さいコンポーネントは理解、テスト、および保守しやすいです。さらに、アプリケーションの複数の部分で使用できるため、再利用性を促進します。

例:

<!-- 前:モノリシックコンポーネント -->
<template>
  <div>
    <Header />
    <Sidebar />
    <MainContent />
    <Footer />
  </div>
</template>
<!-- 後:抽出されたコンポーネントを使用 -->
<template>
  <AppLayout>
    <template v-slot:header><Header /></template>
    <template v-slot:sidebar><Sidebar /></template>
    <template v-slot:main><MainContent /></template>
    <template v-slot:footer><Footer /></template>
  </AppLayout>
</template>

6. コンポーネントCSSをオーバーライドしないようにしてください

コンポーネント外からコンポーネントのCS直接変更することは避けてください。これは予期しない動作につながる可能性があり、アプリケーションのスタイルを維持することが難しくなります。コンポーネント内にスタイルをカプセル化します。

ヒント: プロップまたはスロットを使用してコンポーネントの外観をカスタマイズしてください。これにより、制御されたスタイリングが可能になり、予期しないスタイルの競合を防ぎます。

例:

<!-- Button.vue -->
<template>
  <button :class="['button', variant]" @click="emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
const props = defineProps({
  variant: {
    type: String,
    default: 'primary'
  }
});

const emit = defineEmits(['click']);
</script>
<style scoped>
.button {
  /* 基本ボタンのスタイル */
}
.primary {
  /* プライマリボタンのスタイル */
}
.secondary {
  /* セカンダリボタンのスタイル */
}
</style>