Vue 3のコンポーザブル(Composables)をマスターしよう
Vue 3のComposition APIの核心となるコンポーザブル(Composables)について、実用的な例とベストプラクティスを交えながら詳しく解説します。
コンポーザブルとは?
コンポーザブルは、Vue 3のComposition APIで使用される関数で、再利用可能な状態ロジックをカプセル化します。Reactのカスタムフックと似た概念で、複数のコンポーネント間でロジックを共有できます。
基本的なコンポーザブルの例
// useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset
}
}
<!-- Counter.vue -->
<template>
<div>
<p>カウント: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">リセット</button>
</div>
</template>
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement, reset } = useCounter(10)
</script>
実用的なコンポーザブルの例
1. APIデータフェッチング
// useApi.js
import { ref, onMounted } from 'vue'
export function useApi(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
if (!response.ok) throw new Error('APIエラー')
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(fetchData)
return {
data,
loading,
error,
refetch: fetchData
}
}
2. ローカルストレージ管理
// useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
3. マウス位置の追跡
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
const updateMouse = (event) => {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', updateMouse)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updateMouse)
})
return { x, y }
}
4. フォームバリデーション
// useFormValidation.js
import { ref, computed } from 'vue'
export function useFormValidation() {
const errors = ref({})
const touched = ref({})
const setError = (field, message) => {
errors.value[field] = message
}
const clearError = (field) => {
delete errors.value[field]
}
const touch = (field) => {
touched.value[field] = true
}
const hasError = (field) => {
return touched.value[field] && errors.value[field]
}
const isValid = computed(() => {
return Object.keys(errors.value).length === 0
})
return {
errors,
touched,
setError,
clearError,
touch,
hasError,
isValid
}
}
コンポーザブルのベストプラクティス
1. 命名規則
-
use
で始める(例:useCounter
,useApi
) - 機能を明確に表現する名前を付ける
2. 単一責任の原則
// ❌ 悪い例:複数の責任を持つ
export function useUserAndPosts() {
// ユーザー情報と投稿データの両方を管理
}
// ✅ 良い例:単一の責任
export function useUser() {
// ユーザー情報のみを管理
}
export function usePosts() {
// 投稿データのみを管理
}
3. オプションの提供
// useApi.js
export function useApi(url, options = {}) {
const {
immediate = true,
onSuccess,
onError
} = options
// オプションに基づいて動作を変更
}
4. 適切なライフサイクル管理
// useEventListener.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
5. 型安全性(TypeScript使用時)
// useCounter.ts
import { ref, Ref } from 'vue'
export function useCounter(initialValue: number = 0): {
count: Ref<number>
increment: () => void
decrement: () => void
reset: () => void
} {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset
}
}
コンポーザブルの組み合わせ
複数のコンポーザブルを組み合わせることで、より複雑な機能を実現できます:
<template>
<div>
<form @submit.prevent="handleSubmit">
<input
v-model="formData.email"
@blur="touch('email')"
placeholder="メールアドレス"
/>
<span v-if="hasError('email')" class="error">
{{ errors.email }}
</span>
<button type="submit" :disabled="!isValid">
送信
</button>
</form>
<div v-if="loading">読み込み中...</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useFormValidation } from './useFormValidation'
import { useApi } from './useApi'
const formData = ref({
email: ''
})
const { errors, touched, setError, clearError, touch, hasError, isValid } = useFormValidation()
const { data, loading, error, refetch } = useApi('/api/submit')
const handleSubmit = async () => {
// バリデーション
if (!formData.value.email) {
setError('email', 'メールアドレスは必須です')
return
}
// API送信
await refetch()
}
</script>
まとめ
Vue 3のコンポーザブルは、以下の利点を提供します:
- 再利用性: 複数のコンポーネントでロジックを共有
- テスタビリティ: ロジックを独立してテスト可能
- 可読性: コンポーネントのロジックが整理される
- 型安全性: TypeScriptとの相性が良い
コンポーザブルを活用することで、より保守性が高く、再利用可能なVueアプリケーションを構築できます。まずは簡単な例から始めて、徐々に複雑なロジックをコンポーザブル化していくことをお勧めします。
参考リンク
- Vue 3 Composition API公式ドキュメント
- VueUse - 実用的なコンポーザブルのコレクション