Nuxt 3のcomposablesディレクトリ完全ガイド
結論
Nuxt 3の composables/ ディレクトリは、UIを持たない再利用可能なロジックを配置する場所です。components/ ディレクトリとは明確に役割が異なります。
| components/ | composables/ | |
|---|---|---|
| ファイル形式 | 基本は .vue | .ts または .js |
| UI(template) | あり | なし |
| 用途 | 画面の部品 | 再利用可能なロジック |
| 例 | ボタン、フォーム、カード | useAuth(), useCounter(), useTabSync() |
※ useFetch() や useAsyncData() はNuxt 3の組み込みcomposableです。自作composableと区別して覚えておきましょう。
composableとは何か
Vue公式ドキュメントによると、composableは以下のように定義されています。
In the context of Vue applications, a "composable" is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic.
(Vueアプリケーションのコンテキストにおいて、「composable」はVueのComposition APIを活用してステートフルロジックをカプセル化し、再利用する関数です。)
ステートレスとステートフルの違い
- ステートレスロジック: 入力を受け取り、すぐに出力を返す(例:日付フォーマット関数)
- ステートフルロジック: 時間とともに変化する状態を管理(例:マウス位置の追跡、認証状態)
composableは典型的にはステートフルロジックを扱いますが、ステートレスな共通処理を含めることもあります。
使い分けの目安:
ref,reactive,watchなどを使う →composables/- 純粋な変換関数(日付フォーマット等) →
utils/
※ Nuxt 3では utils/ ディレクトリも自動インポートの対象です。
なぜcomposableを使うのか
1. ロジックの再利用
同じロジックを複数のコンポーネントで使いたい場合、composableに抽出すると1箇所にまとめられます。
Before: 各コンポーネントで同じコードを書く
// ComponentA.vue
<script setup>
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
// ComponentB.vue でも同じコードを書く...
After: composableに抽出
// composables/useMouse.ts
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 各コンポーネントで使用
<script setup>
const { x, y } = useMouse()
</script>
<template>Mouse position: {{ x }}, {{ y }}</template>
2. コードの組織化
大きなコンポーネントを小さな関数に分割できます。関連するロジックをグループ化することで、コードの見通しが良くなります。
3. テストの容易さ
composableは独立した関数なので、コンポーネントに依存せず単体テストを書けます。
命名規則: useプレフィックス
Vue公式の慣例として、composable関数は**use で始まるcamelCase**で命名します。
// ✅ 推奨
useMouse()
useFetch()
useAuth()
useTabQuerySync()
// ❌ 非推奨
getMouse()
fetchData()
authHelper()
この命名規則により、関数がcomposableであることが一目でわかります。React Hooksの useState, useEffect と同じ発想です。
Nuxt 2とNuxt 3の違い
Nuxt 2の時代
Nuxt 2には composables/ ディレクトリは存在しませんでした。再利用可能なロジックは以下の場所に配置されていました。
plugins/- プラグインとして登録utils/- ユーティリティ関数- mixins - Vue 2のコード共有方法
mixinsはVue 2でコンポーネント間のコード共有に使われていましたが、以下の問題がありました。
- 名前の衝突が起きやすい
- どこから来たプロパティか追跡しにくい
- 複数のmixinを組み合わせると複雑になる
Nuxt 3で変わったこと
Nuxt 3では composables/ ディレクトリが導入され、自動インポートが標準機能となりました。
project/
├── composables/
│ ├── useAuth.ts ← 自動インポートされる
│ └── useFetch.ts ← 自動インポートされる
├── components/
│ └── Header.vue
└── pages/
└── index.vue
出典: Nuxt 3 - Composables Directory
自動インポートの仕組み
Nuxt 3では、composables/ ディレクトリ内のファイルは自動的にインポートされます。
自動インポートの注意点
便利な反面、名前の衝突が起きやすい点に注意が必要です。
// composables/useData.ts
export function useData() { ... }
// 別のライブラリにもuseData()がある場合、衝突する
命名規則を決めてプレフィックスをつける(例: useAppData, useMyData)と安全です。
// composables/useCounter.ts
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// pages/index.vue - importなしで使える!
<script setup>
const { count, increment } = useCounter()
</script>
注意: ネストされたディレクトリ
Nuxt公式ドキュメントによると、composables/ のトップレベルのみがデフォルトでスキャンされます。サブディレクトリは自動インポートの対象外です。
※ Nuxtのバージョンや設定によって挙動が異なる場合があります。プロジェクトで確認してください。
composables/
├── useAuth.ts ← ✅ 自動インポートされる
├── useFetch.ts ← ✅ 自動インポートされる
└── validation/
└── useEmail.ts ← ❌ 自動インポートされない
サブディレクトリのcomposableを使う場合は、以下のいずれかの方法を取ります。
方法1: index.tsから再エクスポート(推奨)
// composables/index.ts
export { useEmail } from './validation/useEmail'
方法2: nuxt.config.tsで設定
// nuxt.config.ts
export default defineNuxtConfig({
imports: {
dirs: ['composables/validation']
}
})
実践例: タブ同期のcomposable
実際のプロジェクトで使用したcomposableの例を紹介します。複数のタブコンポーネントで共通するURL同期ロジックを抽出しました。
// composables/useTabQuerySync.ts
interface UseTabQuerySyncOptions {
tabName: string
buildQuery: () => Record<string, string | undefined>
onActivate?: () => void
}
export function useTabQuerySync(options: UseTabQuerySyncOptions) {
const { tabName, buildQuery, onActivate } = options
const router = useRouter()
const currentTab = inject<Ref<string>>('currentTab')
const isInitialized = ref(false)
function updateQueryParams() {
if (!import.meta.client || !isInitialized.value) return
if (currentTab?.value !== tabName) return
const rawQuery = buildQuery()
const query: Record<string, string> = { tab: tabName }
for (const [key, value] of Object.entries(rawQuery)) {
if (value !== undefined && value !== '') {
query[key] = value
}
}
router.push({ query })
}
watch(() => currentTab?.value, (newTab) => {
if (newTab === tabName && isInitialized.value) {
nextTick(() => {
updateQueryParams()
onActivate?.()
})
}
})
function markInitialized() {
isInitialized.value = true
updateQueryParams()
}
return {
currentTab,
isInitialized,
updateQueryParams,
markInitialized,
}
}
使用側のコンポーネント:
// components/tabs/CreditCardTab.vue
<script setup>
const { updateQueryParams, markInitialized } = useTabQuerySync({
tabName: 'creditcard',
buildQuery: () => ({
ccYear: selectedYear.value,
ccMonth: selectedMonth.value,
}),
})
onMounted(async () => {
await loadData()
markInitialized()
})
</script>
このように、7つのタブコンポーネントで同じロジックを再利用できるようになりました。
補足: router.pushとrouter.replace
上記の例では router.push() を使用しています。
router.push(): ブラウザ履歴に追加される(戻るボタンで戻れる)router.replace(): 履歴を置換する(戻るボタンで戻れない)
URL同期の用途では、ユーザーが戻るボタンで前の状態に戻りたい場合は push、履歴を汚したくない場合は replace を選択します。
補足: 型の注意点
buildQuery で数値を返す場合は、明示的に String() で変換しておくと安全です。
buildQuery: () => ({
ccYear: selectedYear.value ? String(selectedYear.value) : undefined,
ccMonth: selectedMonth.value ? String(selectedMonth.value) : undefined,
}),
componentとcomposableの使い分け
迷ったときは以下の基準で判断してください。
componentを使う場合:
- UIを描画する(
<template>が必要) - ユーザーに見える要素を作る
- 例: ボタン、フォーム、カード、モーダル
composableを使う場合:
- UIを持たない
- ロジックだけを再利用したい
ref,watch,computedなどの状態管理- 例: 認証状態管理、API通信、ローカルストレージ操作
まとめ
- composableはVue Composition APIを使った再利用可能なロジック関数
- Nuxt 3の
composables/ディレクトリは自動インポートが効く components/はUI、composables/はロジック、と明確に分離する- 命名は
useXxxの形式で統一する - Nuxt 2のmixinsやplugins内のロジックは、composableに移行すると保守性が上がる