T-CREATOR

Vue 3 入門:Composition API でアプリ開発が変わる!

Vue 3 入門:Composition API でアプリ開発が変わる!

Vue 3 の登場により、フロントエンド開発の世界に革命的な変化がもたらされました。その中でも最も注目すべきは Composition API の導入でしょう。従来の Options API では解決困難だった大規模アプリケーション開発の課題を根本的に解決し、より柔軟で保守性の高いコード記述を可能にします。

本記事では、Composition API の技術的な詳細から実践的な活用方法まで、Vue 3 でアプリ開発がどのように変わるのかを深く掘り下げて解説していきます。従来の開発手法と比較しながら、なぜこの新しいアプローチが必要だったのか、そして実際の開発現場でどのような恩恵をもたらすのかを具体的にご紹介します。

Vue 3 で何が変わったのか

パフォーマンスの飛躍的向上

Vue 3 では、内部アーキテクチャが完全に書き直され、Proxy ベースのリアクティビティシステムが導入されました。これにより、Vue 2 と比較して以下の大幅な改善が実現されています。

#改善項目Vue 2Vue 3改善率
1初期化時間100ms40ms60%減
2メモリ使用量34KB13.5KB60%減
3更新パフォーマンス基準最大 1.3-2 倍向上
4Tree-shaking サポート限定的完全対応革新
5TypeScript 統合プラグイン必要ネイティブサポート革新

新機能の導入

javascript// Vue 3で新たに追加された主要機能
import {
  ref,
  reactive,
  computed,
  watchEffect,
  provide,
  inject,
  Teleport,
  Suspense,
} from 'vue';

これらの新機能により、より表現力豊かで効率的な開発が可能になりました。

Composition API とは何か

概念的定義

Composition API は、Vue 3 で導入された新しいコンポーネント記述方式です。従来の Options API が「オプションオブジェクト」でコンポーネントを定義するのに対し、Composition API は関数型アプローチを採用しています。

javascript// Composition APIの基本構造
<script setup>
  import {(ref, computed, onMounted)} from 'vue' //
  リアクティブデータ const count = ref(0) // 算出プロパティ
  const doubleCount = computed(() => count.value * 2) //
  メソッド const increment = () => {count.value++}
  // ライフサイクル onMounted(() => {console.log(
    'コンポーネントがマウントされました'
  )})
</script>

Options API との根本的な違い

1. コードの構造化方法

Options API の課題

javascript// Options API - 機能が分散
export default {
  data() {
    return {
      userInfo: null,
      loading: false,
      error: null,
    };
  },
  computed: {
    displayName() {
      return this.userInfo?.name || 'Guest';
    },
  },
  methods: {
    async fetchUser() {
      this.loading = true;
      try {
        this.userInfo = await api.getUser();
      } catch (err) {
        this.error = err.message;
      } finally {
        this.loading = false;
      }
    },
  },
  mounted() {
    this.fetchUser();
  },
};

Composition API の解決法

javascript// Composition API - 機能が集約
<script setup>
  import {(ref, computed, onMounted)} from 'vue' import{' '}
  {useUserData} from '@/composables/useUserData' //
  ユーザーデータ関連のロジックが一箇所に集約 const{' '}
  {(userInfo, loading, error, fetchUser)} = useUserData() //
  算出プロパティも近くに配置 const displayName = computed(()
  => userInfo.value?.name || 'Guest') //
  ライフサイクルも同じ場所で管理 onMounted(() =>{' '}
  {fetchUser()})
</script>

2. 型安全性の向上

typescript// Options API - 型推論が困難
export default {
  data() {
    return {
      count: 0 // この時点では型推論が効きにくい
    }
  },
  computed: {
    // thisの型が不安定
    doubleCount(): number {
      return this.count * 2
    }
  }
}

// Composition API - 完全な型安全性
<script setup lang="ts">
import { ref, computed } from 'vue'

const count = ref<number>(0) // 明確な型定義
const doubleCount = computed<number>(() => count.value * 2) // 型推論が効く
</script>

なぜ Composition API が生まれたのか

1. 大規模アプリケーションでの課題

従来の Options API では、コンポーネントが複雑になるにつれて以下の問題が顕在化していました:

javascript// 複雑なOptions APIコンポーネントの例
export default {
  data() {
    return {
      // ユーザー関連
      users: [],
      selectedUser: null,
      userLoading: false,

      // 商品関連
      products: [],
      selectedProduct: null,
      productLoading: false,

      // 注文関連
      orders: [],
      orderLoading: false,

      // UI状態
      activeTab: 'users',
      isModalOpen: false,
    };
  },
  computed: {
    // 200行後にユーザー関連の算出プロパティ
    filteredUsers() {
      /* ... */
    },
    // 400行後に商品関連の算出プロパティ
    filteredProducts() {
      /* ... */
    },
  },
  methods: {
    // 300行後にユーザー関連メソッド
    fetchUsers() {
      /* ... */
    },
    // 500行後に商品関連メソッド
    fetchProducts() {
      /* ... */
    },
  },
};

2. ロジック再利用の困難さ

javascript// Mixinを使った従来の再利用パターンの問題
const UserMixin = {
  data() {
    return {
      users: [], // 名前衝突のリスク
      loading: false, // どのloadingか不明
    };
  },
  methods: {
    fetchUsers() {
      /* ... */
    },
  },
};

const ProductMixin = {
  data() {
    return {
      products: [],
      loading: false, // UserMixinとの名前衝突!
    };
  },
};

Composition API の核となる概念

setup() 関数の役割

setup() 関数は Composition API のエントリーポイントとして機能し、コンポーネントの初期化時に実行されます。

javascript// setup()関数の詳細な動作
<script>
import { ref, reactive, computed, onMounted } from 'vue'

export default {
  // propsとcontextを受け取る
  setup(props, context) {
    console.log('setup()が実行されました')
    console.log('props:', props)
    console.log('context:', context) // { attrs, slots, emit, expose }

    // リアクティブデータの定義
    const count = ref(0)
    const state = reactive({ name: 'Vue' })

    // 算出プロパティ
    const doubleCount = computed(() => count.value * 2)

    // メソッド
    const increment = () => {
      count.value++
      context.emit('count-changed', count.value)
    }

    // ライフサイクル
    onMounted(() => {
      console.log('マウント完了')
    })

    // テンプレートで使用する値を返す
    return {
      count,
      state,
      doubleCount,
      increment
    }
  }
}
</script>

リアクティブ参照(ref、reactive)

ref() の詳細な動作原理

javascriptimport { ref, isRef, unref, toRef } from 'vue';

// 基本的なref
const count = ref(0);
console.log(count.value); // 0
console.log(isRef(count)); // true

// オブジェクトのref
const user = ref({ name: 'Alice', age: 25 });
user.value.name = 'Bob'; // リアクティブに更新される

// ref のヘルパー関数
const unwrapped = unref(count); // ref でない場合はそのまま返す
const nameRef = toRef(user.value, 'name'); // オブジェクトのプロパティをrefに

reactive() の活用パターン

javascriptimport {
  reactive,
  readonly,
  isReactive,
  toRefs,
} from 'vue';

// 基本的なreactive
const state = reactive({
  user: {
    name: 'Alice',
    profile: {
      email: 'alice@example.com',
      settings: {
        theme: 'dark',
      },
    },
  },
  posts: [],
});

// 深いリアクティビティ
state.user.profile.settings.theme = 'light'; // 自動的に更新される

// 読み取り専用版
const readonlyState = readonly(state);
// readonlyState.user.name = 'Bob' // Warning: 変更できない

// プロパティをrefに分割
const { user, posts } = toRefs(state);

算出プロパティ(computed)

javascriptimport { ref, computed, watchEffect } from 'vue';

const firstName = ref('太郎');
const lastName = ref('田中');

// 読み取り専用computed
const fullName = computed(() => {
  console.log('computed が再計算されました');
  return `${lastName.value} ${firstName.value}`;
});

// 読み書き可能computed
const fullNameWritable = computed({
  get() {
    return `${lastName.value} ${firstName.value}`;
  },
  set(value) {
    const names = value.split(' ');
    lastName.value = names[0];
    firstName.value = names[1];
  },
});

// デバッグ機能付きcomputed
const debugComputed = computed(
  () => {
    return firstName.value + lastName.value;
  },
  {
    onTrack(e) {
      console.log('依存関係を追跡:', e);
    },
    onTrigger(e) {
      console.log('更新がトリガーされました:', e);
    },
  }
);

ライフサイクルフック

javascriptimport {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated,
} from 'vue';

export default {
  setup() {
    // マウント前
    onBeforeMount(() => {
      console.log('マウント前の処理');
    });

    // マウント後
    onMounted(() => {
      console.log('DOM要素にアクセス可能');
      // API呼び出し、DOM操作など
    });

    // 更新前後
    onBeforeUpdate(() => {
      console.log('更新前のDOM状態');
    });

    onUpdated(() => {
      console.log('更新後のDOM状態');
    });

    // アンマウント前後
    onBeforeUnmount(() => {
      console.log('クリーンアップ処理');
      // イベントリスナーの削除など
    });

    onUnmounted(() => {
      console.log('完全にアンマウント完了');
    });

    // エラーハンドリング
    onErrorCaptured((err, instance, info) => {
      console.error('子コンポーネントでエラー発生:', err);
      return false; // エラーの伝播を停止
    });
  },
};

実際のコード比較:Options API vs Composition API

複雑なフォーム処理の実装比較

Options API 版

javascript// ContactForm.vue (Options API)
export default {
  data() {
    return {
      form: {
        name: '',
        email: '',
        message: '',
      },
      errors: {},
      loading: false,
      submitted: false,
    };
  },
  computed: {
    isFormValid() {
      return (
        Object.keys(this.errors).length === 0 &&
        this.form.name &&
        this.form.email &&
        this.form.message
      );
    },
    errorCount() {
      return Object.keys(this.errors).length;
    },
  },
  watch: {
    'form.email': {
      handler(newEmail) {
        this.validateEmail(newEmail);
      },
      immediate: true,
    },
    'form.name': {
      handler(newName) {
        this.validateName(newName);
      },
    },
  },
  methods: {
    validateName(name) {
      if (!name) {
        this.$set(this.errors, 'name', '名前は必須です');
      } else if (name.length < 2) {
        this.$set(
          this.errors,
          'name',
          '名前は2文字以上で入力してください'
        );
      } else {
        this.$delete(this.errors, 'name');
      }
    },
    validateEmail(email) {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!email) {
        this.$set(
          this.errors,
          'email',
          'メールアドレスは必須です'
        );
      } else if (!emailRegex.test(email)) {
        this.$set(
          this.errors,
          'email',
          '正しいメールアドレスを入力してください'
        );
      } else {
        this.$delete(this.errors, 'email');
      }
    },
    async submitForm() {
      if (!this.isFormValid) return;

      this.loading = true;
      try {
        await this.$http.post('/api/contact', this.form);
        this.submitted = true;
        this.resetForm();
      } catch (error) {
        this.$toast.error('送信に失敗しました');
      } finally {
        this.loading = false;
      }
    },
    resetForm() {
      this.form = { name: '', email: '', message: '' };
      this.errors = {};
    },
  },
};

Composition API 版

javascript// ContactForm.vue (Composition API)
<script setup>
import { reactive, computed, watch } from 'vue'
import { useFormValidation } from '@/composables/useFormValidation'
import { useApiRequest } from '@/composables/useApiRequest'
import { useToast } from '@/composables/useToast'

// フォームデータ
const form = reactive({
  name: '',
  email: '',
  message: ''
})

// バリデーション機能を分離
const { errors, validateField, isFormValid, errorCount } = useFormValidation({
  name: {
    required: true,
    minLength: 2,
    message: '名前は2文字以上で入力してください'
  },
  email: {
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    message: '正しいメールアドレスを入力してください'
  },
  message: {
    required: true,
    message: 'メッセージは必須です'
  }
})

// API リクエスト機能を分離
const { loading, execute: submitRequest } = useApiRequest()

// 通知機能を分離
const { showError, showSuccess } = useToast()

// リアクティブなバリデーション
watch(() => form.name, (newName) => validateField('name', newName))
watch(() => form.email, (newEmail) => validateField('email', newEmail))
watch(() => form.message, (newMessage) => validateField('message', newMessage))

// フォーム送信
const submitted = ref(false)
const submitForm = async () => {
  if (!isFormValid.value) return

  try {
    await submitRequest('/api/contact', form)
    submitted.value = true
    showSuccess('お問い合わせを送信しました')
    resetForm()
  } catch (error) {
    showError('送信に失敗しました')
  }
}

// フォームリセット
const resetForm = () => {
  Object.assign(form, { name: '', email: '', message: '' })
  errors.value = {}
}
</script>

再利用可能なコンポーザブル

javascript// composables/useFormValidation.js
import { ref, computed } from 'vue';

export function useFormValidation(rules) {
  const errors = ref({});

  const validateField = (fieldName, value) => {
    const rule = rules[fieldName];
    if (!rule) return;

    // 必須チェック
    if (rule.required && !value) {
      errors.value[fieldName] =
        rule.message || `${fieldName}は必須です`;
      return;
    }

    // 最小長チェック
    if (rule.minLength && value.length < rule.minLength) {
      errors.value[fieldName] = rule.message;
      return;
    }

    // パターンチェック
    if (rule.pattern && !rule.pattern.test(value)) {
      errors.value[fieldName] = rule.message;
      return;
    }

    // エラーをクリア
    delete errors.value[fieldName];
  };

  const isFormValid = computed(
    () => Object.keys(errors.value).length === 0
  );
  const errorCount = computed(
    () => Object.keys(errors.value).length
  );

  return {
    errors,
    validateField,
    isFormValid,
    errorCount,
  };
}

// composables/useApiRequest.js
import { ref } from 'vue';
import axios from 'axios';

export function useApiRequest() {
  const loading = ref(false);
  const error = ref(null);

  const execute = async (url, data) => {
    loading.value = true;
    error.value = null;

    try {
      const response = await axios.post(url, data);
      return response.data;
    } catch (err) {
      error.value = err;
      throw err;
    } finally {
      loading.value = false;
    }
  };

  return {
    loading,
    error,
    execute,
  };
}

Composition API で解決される開発課題

ロジックの再利用性

従来の Mixin の問題点

javascript// UserMixin.js - 名前衝突や依存関係が不明確
export default {
  data() {
    return {
      users: [],
      loading: false // 他のMixinと衝突する可能性
    }
  },
  methods: {
    async fetchUsers() {
      this.loading = true
      // APIコール
      this.loading = false
    }
  }
}

// ProductMixin.js - 同じ問題が発生
export default {
  data() {
    return {
      products: [],
      loading: false // UserMixinとの名前衝突!
    }
  }
}

Composition API による解決

javascript// composables/useUsers.js - 完全に独立したロジック
import { ref, reactive } from 'vue'

export function useUsers() {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)

  const fetchUsers = async () => {
    loading.value = true
    error.value = null

    try {
      const response = await api.getUsers()
      users.value = response.data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  const addUser = (user) => {
    users.value.push(user)
  }

  const removeUser = (userId) => {
    users.value = users.value.filter(u => u.id !== userId)
  }

  return {
    // 状態
    users,
    loading,
    error,
    // アクション
    fetchUsers,
    addUser,
    removeUser
  }
}

// 使用例 - 名前衝突なし、依存関係明確
<script setup>
import { useUsers } from '@/composables/useUsers'
import { useProducts } from '@/composables/useProducts'

const {
  users,
  loading: userLoading,
  fetchUsers
} = useUsers()

const {
  products,
  loading: productLoading,
  fetchProducts
} = useProducts()
</script>

型安全性の向上

typescript// TypeScript での Composition API の威力
import { ref, computed, Ref } from 'vue';

// 型安全なコンポーザブル
export function useCounter(initialValue: number = 0) {
  const count: Ref<number> = ref(initialValue);

  const increment = (): void => {
    count.value++;
  };

  const decrement = (): void => {
    count.value--;
  };

  const double = computed<number>(() => count.value * 2);

  return {
    count,
    increment,
    decrement,
    double,
  };
}

// ジェネリクスを活用した高度な型安全性
export function useAsyncState<T>(
  promise: Promise<T>,
  initialState: T
) {
  const state = ref<T>(initialState);
  const loading = ref<boolean>(false);
  const error = ref<Error | null>(null);

  const execute = async (): Promise<void> => {
    loading.value = true;
    error.value = null;

    try {
      const result = await promise;
      state.value = result;
    } catch (err) {
      error.value = err as Error;
    } finally {
      loading.value = false;
    }
  };

  return {
    state,
    loading,
    error,
    execute,
  };
}

大規模アプリケーションでの保守性

javascript// 大規模アプリケーションでの構造化例

// stores/userStore.js - 状態管理の分離
import { reactive, computed } from 'vue';

const state = reactive({
  currentUser: null,
  permissions: [],
  preferences: {},
});

export const useUserStore = () => {
  const isAuthenticated = computed(
    () => !!state.currentUser
  );
  const hasPermission = (permission) =>
    state.permissions.includes(permission);

  const login = async (credentials) => {
    const user = await authAPI.login(credentials);
    state.currentUser = user;
    state.permissions = user.permissions;
  };

  return {
    // 状態
    currentUser: computed(() => state.currentUser),
    permissions: computed(() => state.permissions),
    // 算出プロパティ
    isAuthenticated,
    // アクション
    login,
    hasPermission,
  };
};

// composables/usePermissions.js - 権限ロジックの分離
import { useUserStore } from '@/stores/userStore';

export function usePermissions() {
  const { hasPermission } = useUserStore();

  const canEdit = computed(() => hasPermission('edit'));
  const canDelete = computed(() => hasPermission('delete'));
  const canAdmin = computed(() => hasPermission('admin'));

  return {
    canEdit,
    canDelete,
    canAdmin,
  };
}

// components/UserDashboard.vue - 統合利用
<script setup>
  import {useUserStore} from '@/stores/userStore' import{' '}
  {usePermissions} from '@/composables/usePermissions'
  import {useNotifications} from
  '@/composables/useNotifications' const{' '}
  {(currentUser, isAuthenticated)} = useUserStore() const{' '}
  {(canEdit, canDelete)} = usePermissions() const{' '}
  {(notifications, markAsRead)} = useNotifications() //
  各機能が独立して管理されているため、 // テストや保守が容易
</script>;

実践的な Composition API 活用例

カスタムフック的コンポーザブルの作成

javascript// composables/useLocalStorage.js - ローカルストレージとの連携
import { ref, watch, Ref } from 'vue'

export function useLocalStorage<T>(
  key: string,
  defaultValue: T
): [Ref<T>, (value: T) => void] {

  // 初期値の読み込み
  const storedValue = localStorage.getItem(key)
  const initialValue = storedValue
    ? JSON.parse(storedValue)
    : defaultValue

  const state = ref<T>(initialValue)

  // 状態変更を監視してローカルストレージに保存
  watch(
    state,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue))
    },
    { deep: true }
  )

  // 手動更新関数
  const setValue = (value: T) => {
    state.value = value
  }

  return [state, setValue]
}

// 使用例
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const [userPreferences, setUserPreferences] = useLocalStorage('userPrefs', {
  theme: 'light',
  language: 'ja'
})

// 自動的にローカルストレージと同期される
const toggleTheme = () => {
  userPreferences.value.theme =
    userPreferences.value.theme === 'light' ? 'dark' : 'light'
}
</script>

リアルタイム機能の実装

javascript// composables/useWebSocket.js
import { ref, onUnmounted } from 'vue'

export function useWebSocket(url: string) {
  const socket = ref<WebSocket | null>(null)
  const status = ref<'connecting' | 'connected' | 'disconnected'>('disconnected')
  const lastMessage = ref<any>(null)
  const error = ref<string | null>(null)

  const connect = () => {
    status.value = 'connecting'
    socket.value = new WebSocket(url)

    socket.value.onopen = () => {
      status.value = 'connected'
      error.value = null
    }

    socket.value.onmessage = (event) => {
      lastMessage.value = JSON.parse(event.data)
    }

    socket.value.onerror = (err) => {
      error.value = 'WebSocket エラーが発生しました'
      status.value = 'disconnected'
    }

    socket.value.onclose = () => {
      status.value = 'disconnected'
    }
  }

  const send = (data: any) => {
    if (socket.value && status.value === 'connected') {
      socket.value.send(JSON.stringify(data))
    }
  }

  const disconnect = () => {
    socket.value?.close()
  }

  // コンポーネントがアンマウントされる際の自動クリーンアップ
  onUnmounted(() => {
    disconnect()
  })

  return {
    status,
    lastMessage,
    error,
    connect,
    send,
    disconnect
  }
}

// リアルタイムチャットの実装例
<script setup>
import { ref, watch } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'

const { status, lastMessage, send, connect } = useWebSocket('ws://localhost:8080')
const messages = ref([])
const newMessage = ref('')

// 接続開始
connect()

// 新しいメッセージを監視
watch(lastMessage, (message) => {
  if (message) {
    messages.value.push(message)
  }
})

// メッセージ送信
const sendMessage = () => {
  if (newMessage.value.trim()) {
    send({
      type: 'chat',
      content: newMessage.value,
      timestamp: Date.now()
    })
    newMessage.value = ''
  }
}
</script>

よくあるエラーと解決方法

エラー 1: Cannot access before initialization

bashReferenceError: Cannot access 'count' before initialization

原因setup() 内で変数を定義前に使用している

javascript// ❌ エラーが発生するコード
<script setup>
console.log(count.value) // count がまだ定義されていない

const count = ref(0)
</script>

// ✅ 正しいコード
<script setup>
const count = ref(0)
console.log(count.value) // 定義後に使用
</script>

エラー 2: Cannot read properties of undefined

bashTypeError: Cannot read properties of undefined (reading 'value')

原因:ref や reactive の値が未定義のまま使用されている

javascript// ❌ エラーが発生するコード
<script setup>
const user = ref()
console.log(user.value.name) // user.value が undefined

// ✅ 正しいコード
const user = ref(null)
// 安全なアクセス
console.log(user.value?.name || 'Guest')

// または初期値を設定
const userWithDefault = ref({
  name: '',
  email: ''
})
</script>

エラー 3: reactive オブジェクトの破分割代入エラー

bashWarning: Reactive destructuring will lose reactivity

原因:reactive オブジェクトを直接分割代入している

javascript// ❌ リアクティビティが失われる
<script setup>
const state = reactive({
  count: 0,
  name: 'Vue'
})

const { count, name } = state // リアクティビティが失われる
</script>

// ✅ 正しい方法
<script setup>
import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  name: 'Vue'
})

const { count, name } = toRefs(state) // リアクティビティを保持
</script>

エラー 4: ライフサイクルフックの呼び出しタイミングエラー

bashError: onMounted can only be called during setup()

原因:setup() 関数外でライフサイクルフックを呼び出している

javascript// ❌ エラーが発生するコード
<script setup>
const handleAsyncOperation = async () => {
  await someAsyncTask()
  onMounted(() => { // setup()の同期実行時以外で呼び出し
    console.log('mounted')
  })
}
</script>

// ✅ 正しいコード
<script setup>
// setup()の同期実行時に呼び出し
onMounted(async () => {
  await someAsyncTask()
  console.log('mounted')
})
</script>

エラー 5: TypeScript 型エラー

bashType 'Ref<number>' is not assignable to type 'number'

原因:ref の .value アクセスを忘れている

javascript// ❌ TypeScript エラー
<script setup lang="ts">
const count = ref<number>(0)
const result: number = count // .value が必要

// ✅ 正しいコード
const count = ref<number>(0)
const result: number = count.value

// または unref() を使用
import { unref } from 'vue'
const result: number = unref(count)
</script>

エラー 6: Watch の依存関係エラー

bashWarning: Invalid watch source

原因:watch の第一引数に適切でない値を渡している

javascript// ❌ エラーが発生するコード
<script setup>
const state = reactive({ count: 0 })

watch(state.count, (newVal) => { // 直接プロパティを渡している
  console.log(newVal)
})
</script>

// ✅ 正しいコード
<script setup>
const state = reactive({ count: 0 })

// getter 関数を使用
watch(() => state.count, (newVal) => {
  console.log(newVal)
})

// または toRef を使用
import { toRef } from 'vue'
const countRef = toRef(state, 'count')
watch(countRef, (newVal) => {
  console.log(newVal)
})
</script>

まとめ

Vue 3 の Composition API は、従来の Options API では解決困難だった複数の開発課題を根本的に解決する革新的なアプローチです。

技術的な優位性

  • ロジックの集約: 関連する機能を一箇所にまとめて記述可能
  • 再利用性の向上: コンポーザブル関数による効率的なロジック共有
  • 型安全性: TypeScript との完全な統合による堅牢な開発体験
  • パフォーマンス: より効率的なリアクティビティシステム

開発体験の改善

  • 可読性: 機能ごとにコードが整理され、理解しやすい構造
  • 保守性: 大規模アプリケーションでも管理しやすいアーキテクチャ
  • テスタビリティ: 独立したコンポーザブル関数により単体テストが容易

Composition API を習得することで、より堅牢で保守性の高い Vue.js アプリケーションの開発が可能になります。初めは従来の Options API と併用しながら、徐々に Composition API の活用範囲を広げていくことをおすすめします。

現代のフロントエンド開発において、Composition API は単なる新機能ではなく、開発パラダイムの転換点といえるでしょう。この新しいアプローチを活用して、より効率的で品質の高いアプリケーション開発を実現してください。

関連リンク