Vue 3 の使い方完全ガイド — モダンWeb UIを構築するプログレッシブフレームワーク
一言でいうと
Vue(ビュー)は、リアクティブなデータバインディングとコンポーネントベースのアーキテクチャを中心に設計された、プログレッシブJavaScriptフレームワークです。小規模なインタラクション追加から大規模SPA構築まで、段階的に採用範囲を広げられる柔軟性が最大の特徴です。
この記事は Vue 3.5.32(Composition API中心)を対象としています。
どんな時に使う?
- SPA(シングルページアプリケーション)の構築 — Vue Router・Piniaと組み合わせて、フルスケールのWebアプリケーションを開発する場合
- 既存ページへの部分的なインタラクション追加 — jQueryの代替として、特定のDOM領域だけをVueで管理したい場合
- SSR/SSGを含むフルスタック開発 — Nuxt 3と連携して、SEOに強いサーバーサイドレンダリングや静的サイト生成を行う場合
インストール
# npm
npm install vue
# yarn
yarn add vue
# pnpm
pnpm add vue
プロジェクトを新規作成する場合は、公式のスキャフォールディングツールが便利です:
npm create vue@latest
基本的な使い方
Vue 3で最も標準的な <script setup> + Composition API のパターンです。
<!-- App.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref<number>(0)
const doubled = computed(() => count.value * 2)
function increment(): void {
count.value++
}
</script>
<template>
<div>
<p>カウント: {{ count }}</p>
<p>2倍: {{ doubled }}</p>
<button @click="increment">+1</button>
</div>
</template>
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
<script setup> は Composition API のシンタックスシュガーで、setup() 関数の return やコンポーネント登録を省略できます。Vue 3.5 現在、最も推奨される記述スタイルです。
よく使うAPI — Vue 3 の主要APIの使い方
1. ref / reactive — リアクティブな状態管理
<script setup lang="ts">
import { ref, reactive } from 'vue'
// プリミティブ値には ref
const message = ref<string>('Hello Vue!')
// オブジェクトには reactive
const user = reactive<{ name: string; age: number }>({
name: '太郎',
age: 30,
})
function updateUser(): void {
message.value = 'Updated!' // ref は .value が必要
user.age = 31 // reactive は .value 不要
}
</script>
<template>
<p>{{ message }}</p>
<p>{{ user.name }} ({{ user.age }}歳)</p>
<button @click="updateUser">更新</button>
</template>
2. computed — 算出プロパティ
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref<{ name: string; price: number }[]>([
{ name: 'りんご', price: 150 },
{ name: 'バナナ', price: 100 },
{ name: 'みかん', price: 200 },
])
// 読み取り専用の算出プロパティ
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
)
// 書き込み可能な算出プロパティ
const firstName = ref('太郎')
const lastName = ref('山田')
const fullName = computed({
get: () => `${lastName.value} ${firstName.value}`,
set: (val: string) => {
const [last, first] = val.split(' ')
lastName.value = last
firstName.value = first
},
})
</script>
<template>
<p>合計: {{ totalPrice }}円</p>
<p>氏名: {{ fullName }}</p>
</template>
3. watch / watchEffect — リアクティブな副作用
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref<string>('')
const userId = ref<number>(1)
// 特定のソースを監視(遅延実行)
watch(searchQuery, (newVal, oldVal) => {
console.log(`検索クエリ変更: "${oldVal}" → "${newVal}"`)
// API呼び出しなど
}, { debounce: 300 } as any) // ※ debounce は Vue 本体にはなく、VueUse等で対応
// 複数ソースの監視
watch([searchQuery, userId], ([newQuery, newId], [oldQuery, oldId]) => {
console.log('いずれかが変更されました')
})
// 依存関係を自動追跡(即時実行)
watchEffect(() => {
console.log(`現在の検索: ${searchQuery.value}, ユーザーID: ${userId.value}`)
// searchQuery と userId の両方を自動的に追跡
})
</script>
4. defineProps / defineEmits — コンポーネント間通信
<!-- ChildComponent.vue -->
<script setup lang="ts">
// Props の型定義
interface Props {
title: string
count?: number
items: string[]
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
})
// Emits の型定義
interface Emits {
(e: 'update', id: number): void
(e: 'delete', id: number): void
}
const emit = defineEmits<Emits>()
function handleClick(id: number): void {
emit('update', id)
}
</script>
<template>
<div>
<h2>{{ title }} ({{ count }})</h2>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item }}
<button @click="handleClick(index)">更新</button>
</li>
</ul>
</div>
</template>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
function onUpdate(id: number): void {
console.log(`Item ${id} updated`)
}
</script>
<template>
<ChildComponent
title="買い物リスト"
:count="3"
:items="['りんご', 'バナナ', 'みかん']"
@update="onUpdate"
/>
</template>
5. ライフサイクルフック / provide & inject
<script setup lang="ts">
import {
ref,
onMounted,
onUnmounted,
provide,
inject,
type InjectionKey,
} from 'vue'
// --- ライフサイクルフック ---
const data = ref<string[]>([])
onMounted(async () => {
// DOM マウント後に実行(API呼び出しなど)
const res = await fetch('/api/items')
data.value = await res.json()
})
onUnmounted(() => {
// クリーンアップ処理(タイマー解除、イベントリスナー削除など)
console.log('コンポーネント破棄')
})
// --- provide / inject(依存性注入)---
// 型安全な InjectionKey を定義
const ThemeKey: InjectionKey<string> = Symbol('theme')
// 親コンポーネントで provide
provide(ThemeKey, 'dark')
// 子・孫コンポーネントで inject
const theme = inject(ThemeKey, 'light') // 第2引数はデフォルト値
</script>
類似パッケージとの比較
| 特徴 | Vue 3 | React 18 | Svelte 5 | Angular 17+ |
|---|---|---|---|---|
| アプローチ | プログレッシブ | ライブラリ | コンパイラ | フルフレームワーク |
| リアクティビティ | Proxy ベース(自動追跡) | useState / 手動 | Runes($state) | Signals / Zone.js |
| テンプレート | HTML ベース SFC | JSX | HTML ベース SFC | HTML テンプレート |
| 学習コスト | 低〜中 | 中 | 低 | 高 |
| バンドルサイズ(最小) | ~33KB gzip | ~42KB gzip | ~2KB gzip(ランタイムレス) | ~90KB gzip |
| TypeScript | 公式サポート(良好) | 公式サポート | 公式サポート | 標準(必須に近い) |
| SSR フレームワーク | Nuxt | Next.js | SvelteKit | Angular Universal |
| 状態管理 | Pinia(公式) | Redux / Zustand 等 | 組み込み Store | NgRx / Signals |
選定の目安:
- Vue — テンプレート構文の直感性を重視し、段階的に導入したい場合
- React — エコシステムの広さ・求人数を重視する場合
- Svelte — バンドルサイズ最小化・パフォーマンスを最優先する場合
- Angular — 大規模エンタープライズで統一的なアーキテクチャが必要な場合
注意点・Tips
1. ref vs reactive の使い分け
// ✅ プリミティブ値や置き換えが必要な値は ref
const count = ref(0)
const user = ref<User | null>(null)
user.value = await fetchUser() // 丸ごと置き換え可能
// ✅ ネストしたオブジェクトで .value を避けたい場合は reactive
const form = reactive({ name: '', email: '' })
// ❌ reactive にプリミティブは使えない
// const count = reactive(0) // エラー
// ❌ reactive を分割代入するとリアクティビティが失われる
const { name } = reactive({ name: '太郎' }) // name は非リアクティブ
実務上のおすすめ: 迷ったら ref を使う。ref はあらゆる型に対応でき、一貫性を保てます。
2. ビルドのフィーチャーフラグを設定する
Options API を使わないプロジェクトでは、フィーチャーフラグを設定してバンドルサイズを削減できます。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
define: {
__VUE_OPTIONS_API__: false, // Options API を除外(ツリーシェイキング)
__VUE_PROD_DEVTOOLS__: false, // 本番で DevTools を無効化
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // SSR ミスマッチ詳細を無効化
},
})
3. v-for には必ず :key を付ける
<!-- ❌ key なし — 予期しない再レンダリングの原因 -->
<li v-for="item in items">{{ item.name }}</li>
<!-- ✅ 一意な key を指定 -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
4. shallowRef / shallowReactive でパフォーマンス最適化
大きなオブジェクトや外部ライブラリのインスタンスをリアクティブにする場合、深いリアクティビティは不要なことが多いです。
import { shallowRef, triggerRef } from 'vue'
// 大量データや外部インスタンスには shallowRef
const largeList = shallowRef<Item[]>([])
// 更新時は参照ごと置き換えるか、triggerRef で手動通知
largeList.value = [...largeList.value, newItem]
// または
largeList.value.push(newItem)
triggerRef(largeList)
5. テンプレート内で async は直接使えない
// ❌ テンプレートで await は使えない
// {{ await fetchData() }}
// ✅ onMounted や watch 内で非同期処理を行い、ref に格納
const data = ref<Data | null>(null)
const isLoading = ref(true)
onMounted(async () => {
try {
data.value = await fetchData()
} finally {
isLoading.value = false
}
})
Tip:
<Suspense>コンポーネント(3.5時点で実験的機能)を使えば、async setup()を持つ子コンポーネントのローディング状態を宣言的に扱えます。ただし、本番利用は API の安定化を確認してからにしましょう。
まとめ
Vue 3 は Composition API と <script setup> により、TypeScript との親和性と開発体験が大幅に向上しました。Proxy ベースのリアクティビティシステムは直感的で、ref / computed / watch の3つを押さえるだけで大半のユースケースに対応できます。段階的に導入でき