🎯

Vue 3のコンポーザブル(Composables)について調べてみた

に公開

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アプリケーションを構築できます。まずは簡単な例から始めて、徐々に複雑なロジックをコンポーザブル化していくことをお勧めします。

参考リンク

Discussion