Vue 3 Composition API入門:リアクティブな開発を始めよう
Vue 3のComposition APIの基本から実践的な使い方まで、Options APIとの違いを含めて詳しく解説します。
Vue 3 Composition API 入門
Vue 3 で導入された Composition API は、より柔軟で再利用可能なコードを書くための新しいアプローチです。従来の Options API との違いを理解し、実践的な使い方を学びましょう。
Composition API と Options API の比較
Options API(従来の書き方)
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>投稿数: {{ posts.length }}</p>
<button @click="fetchPosts">投稿を取得</button>
</div>
</template>
<script>
export default {
name: "UserProfile",
data() {
return {
user: {
name: "",
email: "",
},
posts: [],
loading: false,
};
},
computed: {
userDisplayName() {
return `${this.user.name} (${this.user.email})`;
},
},
methods: {
async fetchUser(userId) {
this.loading = true;
try {
const response = await fetch(`/api/users/${userId}`);
this.user = await response.json();
} catch (error) {
console.error(error);
} finally {
this.loading = false;
}
},
async fetchPosts() {
const response = await fetch(`/api/users/${this.user.id}/posts`);
this.posts = await response.json();
},
},
mounted() {
this.fetchUser(1);
},
};
</script>
Composition API(新しい書き方)
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>投稿数: {{ posts.length }}</p>
<button @click="fetchPosts">投稿を取得</button>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from "vue";
export default {
name: "UserProfile",
setup() {
// リアクティブな状態
const user = reactive({
name: "",
email: "",
});
const posts = ref([]);
const loading = ref(false);
// 算出プロパティ
const userDisplayName = computed(() => {
return `${user.name} (${user.email})`;
});
// メソッド
const fetchUser = async (userId) => {
loading.value = true;
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
Object.assign(user, userData);
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
};
const fetchPosts = async () => {
const response = await fetch(`/api/users/${user.id}/posts`);
posts.value = await response.json();
};
// ライフサイクル
onMounted(() => {
fetchUser(1);
});
// テンプレートで使用する値や関数を返す
return {
user,
posts,
loading,
userDisplayName,
fetchPosts,
};
},
};
</script>
基本的なリアクティブ関数
ref と reactive
<script>
import { ref, reactive, isRef, toRefs } from "vue";
export default {
setup() {
// ref: プリミティブ値用
const count = ref(0);
const message = ref("Hello Vue 3!");
// reactive: オブジェクト用
const user = reactive({
name: "John",
age: 25,
hobbies: ["reading", "coding"],
});
// ref値へのアクセス
console.log(count.value); // 0
count.value = 10;
// reactive値へのアクセス
console.log(user.name); // 'John'
user.age = 26;
// 分割代入でリアクティブ性を保持
const { name, age } = toRefs(user);
// ref かどうかの判定
console.log(isRef(count)); // true
console.log(isRef(user)); // false
return {
count,
message,
user,
name,
age,
};
},
};
</script>
computed
<script>
import { ref, computed } from "vue";
export default {
setup() {
const firstName = ref("太郎");
const lastName = ref("山田");
// 読み取り専用の算出プロパティ
const fullName = computed(() => {
return `${lastName.value} ${firstName.value}`;
});
// 書き込み可能な算出プロパティ
const fullNameWritable = computed({
get() {
return `${lastName.value} ${firstName.value}`;
},
set(newValue) {
const names = newValue.split(" ");
lastName.value = names[0];
firstName.value = names[1];
},
});
return {
firstName,
lastName,
fullName,
fullNameWritable,
};
},
};
</script>
watch と watchEffect
<script>
import { ref, watch, watchEffect } from "vue";
export default {
setup() {
const count = ref(0);
const user = reactive({
name: "John",
age: 25,
});
// 単一の値を監視
watch(count, (newValue, oldValue) => {
console.log(`カウントが ${oldValue} から ${newValue} に変更されました`);
});
// 複数の値を監視
watch(
[count, () => user.name],
([newCount, newName], [oldCount, oldName]) => {
console.log(`カウント: ${oldCount} → ${newCount}`);
console.log(`名前: ${oldName} → ${newName}`);
}
);
// オブジェクトの深い監視
watch(
user,
(newUser, oldUser) => {
console.log("ユーザー情報が変更されました", newUser);
},
{ deep: true }
);
// 即座に実行される監視
watch(
count,
(newValue) => {
console.log("初期実行:", newValue);
},
{ immediate: true }
);
// watchEffect: 依存関係を自動追跡
watchEffect(() => {
console.log(`現在のカウント: ${count.value}`);
console.log(`ユーザー名: ${user.name}`);
});
// 監視の停止
const stopWatcher = watchEffect(() => {
console.log(count.value);
});
// 条件によって監視を停止
if (count.value > 10) {
stopWatcher();
}
return {
count,
user,
};
},
};
</script>
ライフサイクル
<script>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
} from "vue";
export default {
setup() {
console.log("setup実行");
onBeforeMount(() => {
console.log("コンポーネントがマウントされる前");
});
onMounted(() => {
console.log("コンポーネントがマウントされた後");
// DOM操作やAPIコールなど
});
onBeforeUpdate(() => {
console.log("コンポーネントが更新される前");
});
onUpdated(() => {
console.log("コンポーネントが更新された後");
});
onBeforeUnmount(() => {
console.log("コンポーネントがアンマウントされる前");
// クリーンアップ処理
});
onUnmounted(() => {
console.log("コンポーネントがアンマウントされた後");
});
onErrorCaptured((error, instance, info) => {
console.log("エラーがキャプチャされました:", error);
return false; // エラーの伝播を停止
});
return {};
},
};
</script>
カスタム Composable 関数
useCounter(再利用可能なロジック)
// composables/useCounter.js
import { ref, computed } from "vue";
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = initialValue;
};
const isEven = computed(() => count.value % 2 === 0);
const isPositive = computed(() => count.value > 0);
return {
count,
increment,
decrement,
reset,
isEven,
isPositive,
};
}
useFetch(API 呼び出し)
// composables/useFetch.js
import { ref, readonly } from "vue";
export function useFetch(url) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const execute = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
// 初回実行
execute();
return {
data: readonly(data),
loading: readonly(loading),
error: readonly(error),
refetch: execute,
};
}
useLocalStorage(ローカルストレージ)
// composables/useLocalStorage.js
import { ref, watch } from "vue";
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key);
const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue;
const value = ref(initialValue);
// 値の変更を監視してローカルストレージに保存
watch(
value,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
},
{ deep: true }
);
return value;
}
実践例:Todo アプリ
<template>
<div class="todo-app">
<h1>Todo アプリ</h1>
<form @submit.prevent="addTodo">
<input v-model="newTodoText" placeholder="新しいタスクを入力" required />
<button type="submit">追加</button>
</form>
<div class="filters">
<button
v-for="filter in filters"
:key="filter.value"
:class="{ active: currentFilter === filter.value }"
@click="currentFilter = filter.value"
>
{{ filter.label }}
</button>
</div>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input type="checkbox" v-model="todo.completed" />
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">削除</button>
</li>
</ul>
<p>残りタスク: {{ remainingCount }} / 全{{ todos.length }}</p>
</div>
</template>
<script>
import { ref, computed } from "vue";
import { useLocalStorage } from "@/composables/useLocalStorage";
export default {
name: "TodoApp",
setup() {
// 状態管理
const todos = useLocalStorage("todos", []);
const newTodoText = ref("");
const currentFilter = ref("all");
const filters = [
{ value: "all", label: "すべて" },
{ value: "active", label: "未完了" },
{ value: "completed", label: "完了" },
];
// 算出プロパティ
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case "active":
return todos.value.filter((todo) => !todo.completed);
case "completed":
return todos.value.filter((todo) => todo.completed);
default:
return todos.value;
}
});
const remainingCount = computed(() => {
return todos.value.filter((todo) => !todo.completed).length;
});
// メソッド
const addTodo = () => {
if (newTodoText.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodoText.value.trim(),
completed: false,
createdAt: new Date(),
});
newTodoText.value = "";
}
};
const removeTodo = (id) => {
const index = todos.value.findIndex((todo) => todo.id === id);
if (index > -1) {
todos.value.splice(index, 1);
}
};
const toggleAll = () => {
const allCompleted = todos.value.every((todo) => todo.completed);
todos.value.forEach((todo) => {
todo.completed = !allCompleted;
});
};
const clearCompleted = () => {
todos.value = todos.value.filter((todo) => !todo.completed);
};
return {
todos,
newTodoText,
currentFilter,
filters,
filteredTodos,
remainingCount,
addTodo,
removeTodo,
toggleAll,
clearCompleted,
};
},
};
</script>
<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
opacity: 0.6;
}
.filters button {
margin-right: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
}
.filters button.active {
background: #007bff;
color: white;
}
</style>
TypeScript との統合
<script lang="ts">
import { defineComponent, ref, computed, PropType } from "vue";
interface User {
id: number;
name: string;
email: string;
}
interface Todo {
id: number;
text: string;
completed: boolean;
userId: number;
}
export default defineComponent({
name: "UserTodos",
props: {
user: {
type: Object as PropType<User>,
required: true,
},
},
emits: {
"todo-added": (todo: Todo) => true,
"todo-updated": (todo: Todo) => true,
},
setup(props, { emit }) {
const todos = ref<Todo[]>([]);
const newTodoText = ref<string>("");
const completedCount = computed<number>(() => {
return todos.value.filter((todo) => todo.completed).length;
});
const addTodo = (): void => {
if (newTodoText.value.trim()) {
const todo: Todo = {
id: Date.now(),
text: newTodoText.value.trim(),
completed: false,
userId: props.user.id,
};
todos.value.push(todo);
emit("todo-added", todo);
newTodoText.value = "";
}
};
const updateTodo = (todo: Todo): void => {
emit("todo-updated", todo);
};
return {
todos,
newTodoText,
completedCount,
addTodo,
updateTodo,
};
},
});
</script>
パフォーマンス最適化
shallowRef と shallowReactive
import { shallowRef, shallowReactive, triggerRef } from "vue";
export default {
setup() {
// 大きなオブジェクトの場合、深いリアクティブ化を避ける
const largeData = shallowRef({
items: new Array(10000)
.fill(0)
.map((_, i) => ({ id: i, data: `item-${i}` })),
});
// 手動でリアクティブ更新をトリガー
const updateLargeData = () => {
largeData.value.items.push({ id: Date.now(), data: "new-item" });
triggerRef(largeData); // 手動更新
};
// 浅いリアクティブオブジェクト
const shallowState = shallowReactive({
nested: {
value: 1, // これは非リアクティブ
},
});
return {
largeData,
updateLargeData,
shallowState,
};
},
};
まとめ
Composition API は、Vue 3 の大きな特徴の一つで、より柔軟で再利用可能なコードを書くことができます。従来の Options API と併用することも可能なので、段階的に移行することができます。
カスタム Composable 関数を活用することで、ロジックの再利用性が高まり、コンポーネントがより保守しやすくなります。TypeScript との組み合わせにより、型安全性も確保できます。
最後まで読んでいただきありがとうございました!てばさん(@basabasa8770)でした!