404 Not Found

404 Not Found


nginx

Vue 3 のカスタムコンポーザブル関数:useMouse、useFetch、useLocalStorage

コンポーザブル関数(フックとも呼ばれる)は、Vue 3のComposition APIにおける中核となる再利用パターンであり、「リアクティブなデータ+ビジネスロジック」を再利用可能な関数にカプセル化します。本質的には、refcomputedwatchなどで構成される「ロジックパッケージ」です。

Composablesを習得することは、保守性の高いVueアプリケーションを作成するための鍵となります。Composablesを使用することで、コンポーネントのコードをより簡潔にでき、ビジネスロジックをコンポーネント間で共有できるようになります。このレッスンでは、独自のComposableライブラリをゼロから構築する方法を学びます。

1. 学習内容


2. ショッピングカート用の「Get + デバウンス」ロジックが5回重複している

(1) 課題:検索ボックスが5つ、重複コードが5箇所

Aliceの管理画面には5つの検索入力フィールド(注文/商品/ユーザーなど)があり、それぞれにデータ取得とデバウンスのロジックが実装されていました:

JS
// ❌ The "Flip" Version:5 component,5 Duplicate code
// OrdersSearch.vue
let timer = null
const searchQuery = ref('')
const results = ref([])

watch(searchQuery, (val) => {
  if (timer) clearTimeout(timer)
  timer = setTimeout(async () => {
    const res = await fetch(`/api/orders?q=${val}`)
    results.value = await res.json()
  }, 300)
})

// ProductsSearch.vue - Exactly the same logic(Change only URL)
// UsersSearch.vue - Exactly the same logic(Change only URL)
// ... And also 2 more ...

合計:50行 × 5 = 250行の重複コード。バグの修正には、5か所の変更が必要です。

プロダクトマネージャーのチャーリーが、新たな要件を追加しました:

「アリス、デバウンスを300msから500msに変更して。それと、最小文字数のチェックを追加して(3文字以上の場合のみ検索するように)。」

アリスは5つのファイルを編集しなければならない。バグが発生しやすい。

(2) Vue Composable ソリューション:1 useSearch、5か所で再利用

JS
// composables/useSearch.js - 1 Composable
import { ref, watch } from 'vue'

export function useSearch(apiUrl, options = {}) {
  const { debounceMs = 300, minLength = 0 } = options
  const searchQuery = ref('')
  const results = ref([])
  const loading = ref(false)
  
  let timer = null
  watch(searchQuery, (val) => {
    if (timer) clearTimeout(timer)
    if (val.length < minLength) {
      results.value = []
      return
    }
    timer = setTimeout(async () => {
      loading.value = true
      const res = await fetch(`${apiUrl}?q=${val}`)
      results.value = await res.json()
      loading.value = false
    }, debounceMs)
  })
  
  return { searchQuery, results, loading }
}
VUE
<!-- OrdersSearch.vue - 1 Line call -->
<script setup>
import { useSearch } from '@/composables/useSearch'
const { searchQuery, results, loading } = useSearch('/api/orders', { debounceMs: 500, minLength: 3 })
</script>

<!-- ProductsSearch.vue - Likewise 1 line (Different URL) -->
<script setup>
import { useSearch } from '@/composables/useSearch'
const { searchQuery, results, loading } = useSearch('/api/products', { debounceMs: 500, minLength: 3 })
</script>

useSearch関数を1か所で定義 → 5か所で再利用。1か所でデバウンスの設定を変更すれば、すべての場所で反映されます。

(3) 収益

『Composable』のその後:


3. Composableの基本

(1) 命名規則

JS
// ✅ Correct:useXxx Naming
useMouse()
useLocalStorage('key')
useFetch('/api/users')
useDebounce(searchQuery, 500)
useToggle(false)

// ❌ Error:Other Names
fetchUser()        // Not based on use Introduction
mouseTracker()     // Not based on use Introduction
getStorage()      // Verb get/set Not included hook

(2) 5つの主な特徴

機能 説明
「use」で始まる 業界標準(Reactも「useXxx」を使用している)
リアクティブデータの返却 ref / 計算済み / リアクティブを返す
許容されるパラメータ 通常、プリミティブ型(文字列/数値/オブジェクト)を受け入れます
単独で使用可能 コンポーネント内に useXxx() と入力するだけ
コンポーザブル 複数のコンポーザブルをネストできる

(3) ファイルの整理

TEXT
src/
├── components/        # Components
├── views/             # Page
├── composables/       # Composable Function(Key Points)
│   ├── useMouse.js
│   ├── useLocalStorage.js
│   ├── useFetch.js
│   ├── useDebounce.js
│   ├── useToggle.js
│   └── index.js       # Batch Export
├── stores/            # Pinia stores
└── App.vue

4. 5つの実用的なコンポーザブルな例

(1) useMouse: マウスの位置を追跡する

JS
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event) {
    x.value = event.clientX
    y.value = event.clientY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}
VUE
<!-- MouseTracker.vue -->
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>

<template>
  <p>Mouse: {{ x }}, {{ y }}</p>
</template>

(2) useLocalStorage: リアクティブな localStorage

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

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored !== null ? JSON.parse(stored) : defaultValue)
  
  watch(data, (val) => {
    localStorage.setItem(key, JSON.stringify(val))
  }, { deep: true })
  
  return data
}
VUE
<!-- ThemeToggle.vue -->
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
</script>

<template>
  <button @click="theme = theme === 'light' ? 'dark' : 'light'">
    Current: {{ theme }}
  </button>
</template>

(3) useFetch:汎用的なデータ取得

JS
// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url.value || url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  // Get It Now
  fetchData()
  
  return { data, loading, error, refetch: fetchData }
}
VUE
<!-- UserList.vue -->
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: users, loading, error, refetch } = useFetch('/api/users')
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error }}</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
  <button @click="refetch">Refresh</button>
</template>

(4) useDebounce: デバウンス値

JS
// composables/useDebounce.js
import { ref, customRef } from 'vue'

export function useDebounce(value, delay = 300) {
  let timer = null
  return customRef((track, trigger) => ({
    get() {
      track()
      return value.value
    },
    set(newValue) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        value.value = newValue
        trigger()
      }, delay)
    }
  }))
}
JS
// Usage
const searchInput = ref('')
const debouncedSearch = useDebounce(searchInput, 500)

watch(debouncedSearch, (val) => {
  console.log('Search:', val)
  // 500ms Execute later
})

(5) useToggle: トグルスイッチ

JS
// composables/useToggle.js
import { ref } from 'vue'

export function useToggle(initialValue = false) {
  const value = ref(initialValue)
  
  function toggle() {
    value.value = !value.value
  }
  
  function setTrue() {
    value.value = true
  }
  
  function setFalse() {
    value.value = false
  }
  
  return { value, toggle, setTrue, setFalse }
}
VUE
<!-- ModalToggle.vue -->
<script setup>
import { useToggle } from '@/composables/useToggle'
const { value: showModal, open: setTrue, close: setFalse } = useToggle(false)
</script>

<template>
  <button @click="setTrue">Open Modal</button>
  <Modal v-if="showModal" @close="setFalse" />
</template>

5. コンポーザブルのベストプラクティス

(1) 6つの主要パラメータの設計

JS
// 1. Basic Parameters
useDebounce(value, 300)

// 2. Configuration Object
useFetch(url, { method: 'POST', body: data })

// 3. Citation Types(Responsive)
useFetch(ref('/api/users'))

// 4. Function Arguments(callback)
useEventListener('click', (e) => console.log(e))

// 5. Multi-parameter
useLocalStorage('key', defaultValue, { mergeDefaults: true })

// 6. Generics(TypeScript)
useLocalStorage<User>('user', { name: '', age: 0 })

(2) 戻り値の設計における5つの重要な考慮事項

JS
// 1. Return directly ref
export function useCounter() {
  const count = ref(0)
  return { count }
}

// 2. Back ref + Methods
export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

// 3. Return to Namespace(Recommendations)
export function useCounter() {
  const count = ref(0)
  return {
    state: { count },
    actions: { increment: () => count.value++ }
  }
}

// 4. Back ref(Deconstructing Friendship)
export function useCounter() {
  return useRef(0)  // Return directly ref,For external use .value
}

// 5. Back readonly(Prevent External Modifications)
import { readonly } from 'vue'
export function useCounter() {
  const count = ref(0)
  return { count: readonly(count) }
}

(3) ライフサイクルの6つの主要なカプセル化

JS
// 1. onMounted + onUnmounted(Most common)
export function useEventListener(event, handler) {
  onMounted(() => window.addEventListener(event, handler))
  onUnmounted(() => window.removeEventListener(event, handler))
}

// 2. watch Automatic Cleanup
export function useWatch(source, callback) {
  const stop = watch(source, callback)
  onUnmounted(() => stop())
}

// 3. setInterval Automatic Cleanup
export function useInterval(fn, delay) {
  let timer = null
  onMounted(() => { timer = setInterval(fn, delay) })
  onUnmounted(() => clearInterval(timer))
}

// 4. setTimeout Automatic Cleanup
export function useTimeout(fn, delay) {
  let timer = null
  onMounted(() => { timer = setTimeout(fn, delay) })
  onUnmounted(() => clearTimeout(timer))
}

// 5. Canceling Asynchronous Tasks
export function useAsyncTask(task) {
  let cancelled = false
  onUnmounted(() => { cancelled = true })
  return async () => {
    if (cancelled) return
    await task()
  }
}

// 6. Route Redirect Cleanup
export function useRouteLeave(callback) {
  onBeforeRouteLeave((to, from) => {
    if (callback()) return false  // Prevent Departure
  })
}

6. VueUse ライブラリ(30以上のコンポーザブル)

(1) VueUseとは何ですか?

VueUseは、Vueコミュニティが提供するComposableライブラリであり、200以上のすぐに使えるComposable(useMouse、useLocalStorage、useDebounce、useEventListenerなど)を提供しているため、自分で記述する手間を省くことができます。

BASH
# Installation
npm install @vueuse/core
JS
// main.js
import { createApp } from 'vue'
import VueUse from '@vueuse/core'
import App from './App.vue'

createApp(App).use(VueUse).mount('#app')

(2) 最もよく使われる5つのVueUseコンポーザブル

JS
import { useMouse, useLocalStorage, useDebounceFn, useEventListener, useToggle } from '@vueuse/core'

// 1. Mouse Position
const { x, y } = useMouse()

// 2. localStorage Responsive
const theme = useLocalStorage('theme', 'light')

// 3. Function Debouncing
const debouncedFn = useDebounceFn(() => {
  console.log('Debounced!')
}, 500)

// 4. Global Event Listeners
useEventListener('resize', () => {
  console.log('Window resized')
})

// 5. Switch Toggle
const [value, toggle] = useToggle()

(3) 5つの主なユースケース

シナリオ VueUse Composable
マウスの位置 useMouse / useMouseInElement
スクロール位置 useScroll / useInfiniteScroll
localStorage useLocalStorage / useStorage
ネットワークの状態 useOnline / useNetwork
メディアクエリ useMediaQuery / useBreakpoints
全画面表示 useFullscreen
クリップボード useClipboard
マウスのドラッグ useDraggable
要素のサイズ useElementSize / useResizeObserver
デバウンス / スロットル useDebounceFn / useThrottleFn

7. 完全な例:5つのコンポーザブルの組み合わせ

(1) ▶ サンプル:. useMouse の完全な実装

JS
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event) {
    x.value = event.clientX
    y.value = event.clientY
  }
  
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
  
  return { x, y }
}
▶ 試してみよう

(2) ▶ サンプル:. useFetch の完全な実装

JS
import { ref, watch, isRef } from 'vue'

export function useFetch(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  async function fetchData() {
    const requestUrl = isRef(url) ? url.value : url
    if (!requestUrl) return
    
    loading.value = true
    error.value = null
    try {
      const res = await fetch(requestUrl, options)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  watch(url, fetchData, { immediate: true })
  
  return { data, loading, error, refetch: fetchData }
}
▶ 試してみよう

(3) ▶ サンプル:. useLocalStorage の完全な実装

JS
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored !== null ? JSON.parse(stored) : defaultValue)
  
  watch(data, (val) => {
    localStorage.setItem(key, JSON.stringify(val))
  }, { deep: true })
  
  // Cross-Tab Synchronization
  window.addEventListener('storage', (e) => {
    if (e.key === key && e.newValue) {
      data.value = JSON.parse(e.newValue)
    }
  })
  
  return data
}
▶ 試してみよう

例:4. useDebounce + useThrottle

JS
import { ref, customRef } from 'vue'

// Image Stabilization:After the last operation delay Trigger
export function useDebounce(value, delay = 300) {
  let timer = null
  return customRef((track, trigger) => ({
    get() {
      track()
      return value.value
    },
    set(newValue) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        value.value = newValue
        trigger()
      }, delay)
    }
  }))
}

// Cost-cutting: Max 1 trigger during delay period
export function useThrottle(value, delay = 300) {
  let last = 0
  return customRef((track, trigger) => ({
    get() {
      track()
      return value.value
    },
    set(newValue) {
      const now = Date.now()
      if (now - last >= delay) {
        last = now
        value.value = newValue
        trigger()
      }
    }
  }))
}

(4) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス

エラー 症状 解決策
「use」で始まらない チームで認識されていない 「useXxx」という名前
通常のオブジェクトを返す リアクティブ性を失う 参照を返す
クリーンアップを行わないことによる副作用 メモリリーク onUnmounted でのクリーンアップ
過度な抽象化 単純なシナリオではコンポーザブルを使用する 単純なロジックはコンポーネント内に直接記述する
5段階のネストされたComposable デバッグが難しい サブコンポーネントやPiniaに分解する

(5) ▶ サンプル:. 上位5つのコンポーザブルの性能比較

パターン 再利用性 パフォーマンス 保守性 適用性
コピー&ペースト ⭐⭐⭐ 1回限り
ミックスイン (Vue 2) ⭐⭐ ⭐⭐ ⭐⭐ レガシープロジェクト
Composable ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ おすすめ
Pinia ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ 大規模アプリ
イベントバス ⭐⭐⭐ ⭐⭐⭐ ⭐⭐ コンポーネント間の通信

❓ よくある質問

Q コンポーザブルとミックスインの違いは何ですか?
A ミックスインは Vue 2 で採用されていた再利用のアプローチ(暗黙的なマージ、名前衝突)です。コンポーザブルは Vue 3 で採用されているアプローチ(明示的な返却、型安全)です。Vue 3 ではコンポーザブルの使用が推奨されています。
Q コンポーザブルは setup の最上位レベルで呼び出す必要がありますか?
A はい。コンポーザブルは ref、ウォッチ、ライフサイクルフックを使用するため、<script setup> の最上位レベルで呼び出す必要があります。
Q コンポーザブルはプロップを受け付けますか?
A プロップを直接受け付けることはありません。ただし、ref(リアクティブ)や単純な値を受け付けることは可能です。VueUse ライブラリでは、ref パラメータが広く活用されています。
Q コンポーザブルは互いに呼び出すことができますか?
A はい。コンポーザブルは本質的に関数であり、ネストすることができます。例えば、useSearch には useFetch + useDebounce を記述することができます。
Q いつ独自のコードを書き、いつVueUseを使うべきですか?
A ビジネス固有のロジック(useSearchやuseCartなど)については独自のコードを記述してください。汎用的なロジック(マウスイベント、スクロール、デバウンス、フルスクリーンモードなど)にはVueUseを使用することで、作業時間を80%削減できます。
Q ComposableはReact Hooksのコピーですか?
A はい、React Hooksから着想を得ています。Vue 3はReact Hooksの仕組みを取り入れ、さらに改良を加えています。具体的には、refs(自動依存関係収集)と、トップレベルでのセットアップ呼び出し(useEffectでラップする必要がない)を組み合わせています。
Q Composable をテストするにはどうすればよいですか?
A Composable は通常の JavaScript 関数です。直接呼び出して const { data } = useFetch('/api') を行い、data.value でアサーションを実行してください。ユニットテストには Vitest を使用してください。

📖 まとめ


📝 練習問題

(1) 基礎問題(難易度:⭐)

useToggle 関数を実装してください:

(2) 上級問題(難易度:⭐⭐)

useFetchのフルバージョンの実装:

(3) チャレンジ問題(難易度:⭐⭐⭐)

完全な「eコマース用バックエンド・コンポーザブル・ライブラリ」を実装する:

  1. 5つのコンポーザブル:useCart / useAuth / useSearch / usePagination / useTable
  2. 各Composableには、少なくとも50行の実装コードと完全なテストが含まれている必要があります
  3. すべてを composables/index.js にエクスポートする
  4. 5つのテストケースを作成する(Vitestを使用)
  5. 自身の実装とVueUseライブラリの実装を比較してください
  6. 各コンポーザブルのAPIを文書化する
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%