Chuyển đến nội dung chính

レッスン 7: コンポーザブルと再利用可能なロジック

カスタム コンポーザブル、VueUse ライブラリ。 useAsyncState、useFetch、useLocalStorage。構成可能なパターン、共有状態。コンポーザブルによる依存関係の注入。

💻 プログラミング — レッスン 7 レッスン 7: コンポーザブルと再利用可能なロジック

Vue.js と Nuxt: 基本から高度まで

パート 2: Vue エコシステム

xdev.asia

1. コンポーザブルとは何ですか?

Composable は、Composition API を使用してステートフル ロジックをカプセル化して再利用する関数です。規則: で始まる名前を付けます。 使う。

// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'

export function useFetch<T>(url: string | Ref<string>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  async function execute() {
    isLoading.value = true
    error.value = null
    try {
      const rawUrl = typeof url === 'string' ? url : url.value
      const res = await fetch(rawUrl)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      isLoading.value = false
    }
  }

  watchEffect(() => { execute() })

  return { data, error, isLoading, refresh: execute }
}

2. コンポーザブルを使用する

<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'

interface Post {
  id: string
  title: string
}

const { data: posts, isLoading, error, refresh } = useFetch<Post[]>('/api/posts')
</script>

<template>
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else>
    <button @click="refresh">Refresh</button>
    <div v-for="post in posts" :key="post.id">{{ post.title }}</div>
  </div>
</template>

3. 一般的なコンポーザブル

// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data
}

// composables/useDebounce.ts
import { ref, watch, type Ref } from 'vue'

export function useDebounce<T>(value: Ref<T>, delay = 300) {
  const debounced = ref(value.value) as Ref<T>

  let timeout: ReturnType<typeof setTimeout>
  watch(value, (newVal) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debounced.value = newVal
    }, delay)
  })

  return debounced
}

// composables/useMediaQuery.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMediaQuery(query: string) {
  const matches = ref(false)

  let mediaQuery: MediaQueryList

  onMounted(() => {
    mediaQuery = window.matchMedia(query)
    matches.value = mediaQuery.matches
    mediaQuery.addEventListener('change', handler)
  })

  function handler(e: MediaQueryListEvent) {
    matches.value = e.matches
  }

  onUnmounted(() => {
    mediaQuery?.removeEventListener('change', handler)
  })

  return matches
}

4. VueUseライブラリ

npm install @vueuse/core
<script setup lang="ts">
import {
  useDark,
  useToggle,
  useLocalStorage,
  useWindowSize,
  useIntersectionObserver,
  onClickOutside,
} from '@vueuse/core'

// Dark mode
const isDark = useDark()
const toggleDark = useToggle(isDark)

// Persistent state
const settings = useLocalStorage('settings', { theme: 'light', lang: 'vi' })

// Window size
const { width, height } = useWindowSize()

// Intersection observer
const target = ref(null)
const isVisible = ref(false)
useIntersectionObserver(target, ([entry]) => {
  isVisible.value = entry.isIntersecting
})

// Click outside
const dropdown = ref(null)
onClickOutside(dropdown, () => { isOpen.value = false })
</script>

5. 構成可能なパターン

// Pattern: Shared state across components
// composables/useSharedCounter.ts
const globalCount = ref(0) // Shared state

export function useSharedCounter() {
  function increment() { globalCount.value++ }
  function decrement() { globalCount.value-- }
  return { count: globalCount, increment, decrement }
}

次の記事: フォーム、検証、スタイル設定 — VeeValidate、Tailwind CSS、コンポーネント ライブラリ。