• #Nuxt
  • #Vue
  • #Composition API
  • #フロントエンド
開発未分類

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を活用してステートフルロジックをカプセル化し、再利用する関数です。)

出典: Vue.js - Composables

ステートレスとステートフルの違い

  • ステートレスロジック: 入力を受け取り、すぐに出力を返す(例:日付フォーマット関数)
  • ステートフルロジック: 時間とともに変化する状態を管理(例:マウス位置の追跡、認証状態)

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に移行すると保守性が上がる

参考リンク