この記事の要点
• ref()とreactive()でリアクティブな状態管理
• <script setup>で簡潔に Composition API を記述
• computed()で算出プロパティ、watch()で副作用を実装
リアクティビティ基礎
| API | 説明 |
|---|---|
ref(value) | プリミティブ値をリアクティブ化 |
reactive(obj) | オブジェクトをリアクティブ化 |
computed(() => ...) | 算出プロパティ |
watch(source, callback) | 監視 |
watchEffect(() => ...) | 自動依存追跡の監視 |
<script setup>
import { ref, reactive, computed } from 'vue'
const count = ref(0)
const user = reactive({
name: 'Alice',
age: 25
})
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
</script>
<template>
<p>{{ count }} - {{ doubled }}</p>
<button @click="increment">+1</button>
<p>{{ user.name }} ({{ user.age }})</p>
</template>
ポイント: refは.valueでアクセス、reactiveはそのままアクセスします。テンプレート内では.valueが自動でアンラップされます。
テンプレート構文
| 構文 | 説明 |
|---|---|
{{ message }} | テキスト補間 |
v-bind:src="url" / :src="url" | 属性バインディング |
v-on:click="handler" / @click="handler" | イベントリスナー |
v-if="condition" | 条件付きレンダリング |
v-for="item in items" | リストレンダリング |
v-model="value" | 双方向バインディング |
<template>
<img :src="imageUrl" :alt="title">
<button @click="handleClick">Click</button>
<div v-if="isVisible">表示中</div>
<div v-else>非表示</div>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<input v-model="text" type="text">
</template>
実践メモ: v-forには必ず:keyを指定します。一意なキーがないと、リストの更新時にパフォーマンスが劣化したり、状態が崩れたりします。
ライフサイクルフック
| フック | タイミング |
|---|---|
onBeforeMount | マウント前 |
onMounted | マウント後(DOM 操作可能) |
onBeforeUpdate | 更新前 |
onUpdated | 更新後 |
onBeforeUnmount | アンマウント前 |
onUnmounted | アンマウント後 |
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const count = ref(0)
let timer = null
onMounted(() => {
console.log('Component mounted')
timer = setInterval(() => {
count.value++
}, 1000)
})
onUnmounted(() => {
console.log('Component unmounted')
clearInterval(timer)
})
</script>
ポイント: onMountedで DOM が利用可能になります。API 呼び出しや DOM 操作はここで実行します。onUnmountedでタイマーやイベントリスナーをクリーンアップします。
コンポーネント Props と Emit
<!-- Child.vue -->
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
const emit = defineEmits(['increment', 'decrement'])
function handleClick() {
emit('increment', props.count + 1)
}
</script>
<template>
<h2>{{ title }}</h2>
<p>{{ count }}</p>
<button @click="handleClick">+1</button>
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
function onIncrement(newValue) {
count.value = newValue
}
</script>
<template>
<Child :title="My Counter" :count="count" @increment="onIncrement" />
</template>
注意: propsは読み取り専用です。子コンポーネントから直接変更できません。親に変更を通知する場合はemitを使います。
Composables(再利用ロジック)
// 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
}
}
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement } = useCounter(10)
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</template>
実践メモ: Composablesはuseプレフィックスの関数で、状態とロジックを再利用できます。複数コンポーネントで同じロジックを使う場合に便利です。
Vue Router
| API | 説明 |
|---|---|
<router-link to="/path"> | ナビゲーションリンク |
<router-view /> | ルートコンポーネント表示 |
useRouter() | ルーターインスタンス取得 |
useRoute() | 現在のルート情報取得 |
// router.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/user/:id', component: User }
]
export const router = createRouter({
history: createWebHistory(),
routes
})
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
function goToAbout() {
router.push('/about')
}
console.log(route.params.id)
</script>
<template>
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</nav>
<router-view />
</template>
ポイント: useRouterでナビゲーション操作、useRouteでパラメータ取得します。router.pushはプログラムから遷移するときに使います。
Pinia(状態管理)
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubled,
increment
}
})
<script setup>
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
</script>
<template>
<p>{{ counter.count }} - {{ counter.doubled }}</p>
<button @click="counter.increment">+1</button>
</template>
実践メモ: Piniaは Vuex の後継で、Composition API と同じ記法で書けます。型推論も強力で、TypeScript との相性が良いです。
フォームバインディング
| 要素 | v-model の動作 |
|---|---|
<input type="text"> | value 属性をバインド |
<textarea> | value 属性をバインド |
<input type="checkbox"> | checked 属性をバインド |
<input type="radio"> | checked 属性をバインド |
<select> | value 属性をバインド |
<script setup>
import { ref } from 'vue'
const text = ref('')
const checked = ref(false)
const selected = ref('')
</script>
<template>
<input v-model="text" type="text">
<input v-model="checked" type="checkbox">
<select v-model="selected">
<option value="A">Option A</option>
<option value="B">Option B</option>
</select>
</template>
| 修飾子 | 説明 |
|---|---|
v-model.lazy | change イベントで同期(input ではなく) |
v-model.number | 数値型に自動変換 |
v-model.trim | 前後の空白を削除 |
ポイント: v-model.numberを使うと、input[type="number"]の値が文字列ではなく数値になります。計算処理で型変換が不要になります。
クラスとスタイルバインディング
<template>
<div :class="{ active: isActive, 'text-danger': hasError }">
テキスト
</div>
<div :class="[baseClass, { active: isActive }]">
配列形式
</div>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
スタイル
</div>
</template>
実践メモ: :classのオブジェクト構文で、条件に応じてクラスを付け外しできます。静的クラスと併用も可能です。
スロット
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">デフォルトヘッダー</slot>
</div>
<div class="card-body">
<slot>デフォルト本文</slot>
</div>
</div>
</template>
<!-- 使用側 -->
<Card>
<template #header>
<h2>タイトル</h2>
</template>
<p>本文テキスト</p>
</Card>
ポイント: スロットでコンポーネントの一部を親から注入できます。nameで複数のスロットを定義できます。
参考リソース
- Vue 3 Documentation - 公式ドキュメント
- Vue Router Documentation - Vue Router 公式
- Pinia Documentation - Pinia 公式
- Vue 3 Migration Guide - Vue 2 からの移行ガイド
関連記事
- TypeScript チートシート - TypeScript のリファレンス
- JavaScript 基礎 - JavaScript の基本を復習