2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

参考リンク

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?