Vue Composable入門:なぜ必要?具体的なコード例で学ぶ

Vue Composable入門:なぜ必要?具体的なコード例で学ぶ

はじめに:現代Vue開発とComposition API、そしてComposable

Vue.jsは、その分かりやすいAPIと柔軟性から、多くの開発者に愛されています。特にバージョン3では、リアクティブシステムの刷新に加え、Composition APIという強力な新機能が導入されました。これは、従来のOptions APIにおけるいくつかの課題を解決し、より大規模で複雑なアプリケーション開発を効率的に行うためのものです。

Composition APIは、コンポーネントのロジックを「関心事 (concern)」ごとに整理することを可能にします。そして、このComposition APIの能力を最大限に引き出すためのパターンこそが、「Composable」です。

初めて「Composable」という言葉を聞く方、Composition APIを使い始めたけれどComposableの利点がよく分からない方、あるいはOptions APIから移行を検討している方にとって、この記事はComposableの世界への最初の一歩となるでしょう。

この記事では、まず従来のOptions APIが抱えていた課題を振り返り、なぜComposition API、そしてComposableが必要とされたのかを解説します。次に、Composableとは具体的に何なのか、その基本的な構造とメリットを詳細に説明します。そして、理論だけでなく、実際のコード例を通してComposableの強力さと便利さを体験していただきます。簡単なカウンターから、マウス座標の追跡、非同期データのフェッチ、フォーム検証といった、様々な開発シーンで役立つComposableの例を取り上げ、その使い方とメリットを具体的に示します。

この記事を読み終える頃には、Composableが現代Vue開発においていかに強力なツールであり、なぜあなたのプロジェクトで活用すべきなのかが明確に理解できるはずです。さあ、Composableの世界へ飛び込みましょう。

Options APIの課題とComposableへの道

Vue 2までの開発の主流であったOptions APIは、data, methods, computed, watch, ライフサイクルフック (mounted, createdなど) といった、コンポーネントの様々な「オプション」に関心事を分散させる構造を持っていました。これはシンプルで分かりやすい反面、特に大規模なコンポーネントや、複数のコンポーネントで同じロジックを共有したい場合にいくつかの課題を生じさせていました。

Options APIにおけるコードの分散と可読性の低下

例えば、あるコンポーネントが「データのフェッチ」「ユーザー入力の検証」「イベントリスナーの管理」という3つの機能を持つとします。Options APIでは、これらの機能に関連するコードは、コンポーネントの様々なオプションに分散して記述されることになります。

  • データの状態 (data)
  • データをフェッチするメソッド (methods)
  • ローディング状態やエラー状態 (data, computed)
  • データの変更を監視する (watch)
  • コンポーネントがマウントされたときにデータをフェッチ (mounted)
  • 入力データの状態 (data)
  • 入力データの検証ロジック (methods, computed)
  • 検証エラーの状態 (data, computed)
  • イベントリスナーを登録 (mounted)
  • イベントリスナーを解除 (beforeDestroy / unmounted)

このように、一つの機能(例えばデータのフェッチ)に関連するコードが、data, methods, mounted など、複数のオプションに散らばってしまいます。コンポーネントが複雑になるにつれて、特定の機能に関連するコードを追いかけるのが難しくなり、コードの可読性や保守性が低下するという問題がありました。

ロジック再利用の難しさ:Mixinsの限界

Options APIで複数のコンポーネント間でロジックを再利用する主な方法として、「Mixins」がありました。Mixinsは、複数のコンポーネントオプションをマージする機能を提供します。例えば、複数のコンポーネントで共通のデータフェッチロジックを使いたい場合に、そのロジックをMixinとして定義し、各コンポーネントでインポートして使用します。

しかし、Mixinsにはいくつかの問題点がありました。

  1. 名前の衝突 (Name Conflicts): Mixinsが提供するdataプロパティやmethods名が、使用するコンポーネント自身のプロパティやメソッド名と衝突する可能性がありました。これは、Mixinsが「透過的に」マージされるため、コンポーネント側からはMixinが何を提供しているのかを完全に把握しておく必要があり、予測が難しい問題でした。
  2. 依存関係の不明瞭さ: Mixinがコンポーネント内の他のプロパティやメソッドに依存している場合、その依存関係がコード上で明確になりにくいという問題がありました。Mixinの中を読まないと、それが何に依存しているのか、どのように動作するのかが分かりにくいため、保守が困難になることがありました。
  3. 追跡の難しさ: ある値やメソッドがどこから来ているのか(そのコンポーネント自身で定義されているのか、それとも複数のMixinのどれかから来ているのか)を追いかけるのが難しくなり、デバッグや理解に時間がかかりました。
  4. Mixinsの連鎖による複雑化: 複数のMixinを組み合わせることで、さらに問題が複雑化し、コードの挙動を把握するのが非常に困難になるケースが見られました。

これらのMixinsの課題は、大規模なアプリケーションにおいて、ロジックの再利用性を高めようとするほど、かえってコードベースを理解しにくく、保守しにくいものにしてしまう傾向がありました。

Composition APIの登場:関心事ごとの整理

Vue 3で導入されたComposition APIは、これらのOptions APIの課題への解決策として登場しました。Composition APIは、setup() 関数を中心に、ref, reactive, computed, watch といったリアクティブプリミティブや、onMounted, onUnmounted といったライフサイクルフックを直接インポートして使用します。

setup() 関数の中では、コンポーネント内で使用するリアクティブな状態や関数を自由に定義し、それらをオブジェクトとして返します。返されたプロパティやメソッドは、テンプレートから参照できるようになります。

Composition APIの最大の特徴は、特定の機能(関心事)に関連する状態、ロジック、ライフサイクルフックを、setup() 関数内でまとめて記述できる点です。これにより、Options APIのように関心事がコンポーネントの様々なオプションに分散することがなくなり、コードの可読性や保守性が向上します。

しかし、Composition APIだけでもまだ不十分なケースがあります。もし同じ関心事(例えばデータのフェッチロジック)を複数のコンポーネントで再利用したい場合は、Composition APIのコードブロックをコピー&ペーストするか、あるいは別の方法を考える必要があります。ここで登場するのが「Composable」です。

Composable:Composition APIを再利用可能な関数としてカプセル化する

Composableとは、状態フルなロジックをカプセル化し、複数のコンポーネント間で再利用可能にするための関数です。Composition APIで記述された特定の関心事に関するコードブロックを、独立したJavaScript関数として切り出すことで作成します。

このComposable関数は、内部でref, reactive, computedを使って状態を宣言したり、watchで変更を監視したり、onMountedなどのライフサイクルフックを登録したりすることができます。そして、コンポーネントで必要な状態や関数を、そのComposable関数の戻り値として提供します。

Composableは、Mixinsが抱えていた問題点の多くを解決します。

  • 明確な入力と出力: Composableはプレーンな関数なので、引数(入力)と戻り値(出力)が明確です。これにより、Composableが何に依存し、何を提供するのかが一目で分かりやすくなります。
  • 名前の衝突の回避: Composableから返されるプロパティやメソッドは、コンポーネントのsetup()関数内で分割代入などを使って変数に割り当てられます。これにより、命名の自由度が高まり、意図しない名前の衝突を回避できます。
  • 追跡の容易さ: Composableから提供される機能は、setup()関数内の特定の変数に格納されるため、その値やメソッドがどこから来ているのかを簡単に追跡できます。
  • より柔軟なロジックの組み合わせ: Composableは関数なので、引数を渡したり、複数のComposableを組み合わせて新しいComposableを作成したりといった、柔軟な方法でロジックを構築できます。

つまり、ComposableはComposition APIで記述されたロジックを、より構造化され、再利用可能で、テストしやすい形でパッケージ化するためのパターンなのです。Options APIの課題をComposition APIが解決し、そのComposition APIの再利用性という側面を最大限に引き出すのがComposableの役割と言えます。

Composableとは何か?より深く理解する

改めて、Composableとは何でしょうか。公式ドキュメントでは「状態フルなロジックをカプセル化し、再利用可能にする関数」と定義されています。もう少しブレークダウンしてみましょう。

  1. 関数であること: Composableは、特別な構文や構造を持つものではなく、単なるJavaScript関数です。このシンプルさが、その柔軟性とテスト容易性の基盤となります。
  2. 状態フルなロジックをカプセル化: Composableの内部では、refreactiveを使用してリアクティブな状態を宣言・管理したり、computedで派生状態を定義したり、watchで状態の変化を監視したりします。また、onMounted, onUnmounted, onUpdated などのライフサイクルフックを呼び出すことも可能です。これにより、特定の機能(例えば、マウス座標の追跡)に必要な状態 (x, y 座標) と、その状態を操作・更新するロジック(イベントリスナーの追加/削除、座標の更新処理)を一箇所にまとめることができます。
  3. 再利用可能: Composable関数を定義すれば、それを複数の異なるコンポーネントのsetup()関数内から何度でも呼び出すことができます。呼び出すたびに、そのComposableは独立した状態インスタンスを提供します(シングルトンにしたい場合は別のパターンが必要ですが、基本的なComposableの呼び出しは独立したインスタンスを生成します)。
  4. 慣習的な命名規則: Composable関数は、通常 use というプレフィックスを付けて命名されます(例: useMousePosition, useFetchData, useFormValidation)。これは強制ではありませんが、Composableであることが一目で分かり、アプリケーション内の他のユーティリティ関数などと区別しやすくなるため、強く推奨される慣習です。

Composableは、Composition APIの上に構築されています。つまり、Composition APIが提供するref, reactive, ライフサイクルフックといった機能がなければ、Composableは成り立ちません。Composition APIが関心事ごとのロジック整理を可能にし、Composableはその整理されたロジックを再利用可能な単位にパッケージ化する役割を果たします。

どのように機能するか?

Composable関数をコンポーネントのsetup()関数内で呼び出すと、そのComposable内で定義されたリアクティブな状態や関数が利用可能になります。

“`javascript
// useMousePosition.js
import { ref, onMounted, onUnmounted } from ‘vue’

export function useMousePosition() {
const x = ref(0)
const y = ref(0)

function update(e) {
x.value = e.pageX
y.value = e.pageY
}

onMounted(() => window.addEventListener(‘mousemove’, update))
onUnmounted(() => window.removeEventListener(‘mousemove’, update))

return { x, y } // 外部に公開したい状態や関数を返す
}
“`

“`vue



“`

この例では、useMousePositionというComposableがマウス座標の状態 (x, y) と、それを更新するためのイベントリスナー管理ロジックをカプセル化しています。MouseTracker.vueコンポーネントは、単純にこの Composable を呼び出し、返された xy をテンプレートで表示しているだけです。マウス座標追跡に関する複雑なロジックは Composable 内に閉じ込められており、コンポーネント自身は非常にシンプルで、その「何を表示するか」というUIの責務に集中できています。

もし別のコンポーネントでもマウス座標を使いたい場合、そのコンポーネントのsetup()関数内で同じようにuseMousePosition()を呼び出すだけで、簡単に同じ機能を利用できます。各コンポーネントで呼び出されたuseMousePosition()は、それぞれ独立したxyの状態を持ち、他のコンポーネントに影響を与えません。

このように、Composableは特定の機能に関連するロジックを、独立した再利用可能な単位として cleanly に分離することを可能にします。

なぜComposableが必要なのか? (メリットの詳細)

Composableの基本的な理解が進んだところで、なぜこれが現代Vue開発、特にComposition APIを使った開発において強力で必要不可欠なパターンとされているのか、その具体的なメリットを掘り下げていきましょう。

1. ロジックの再利用性の向上

これは Composable の最も主要なメリットです。Options API の Mixins が抱えていた問題点を解決しつつ、状態フルなロジックを簡単に複数のコンポーネントで共有できます。

  • 例: アプリケーション内で複数の場所で特定のAPIからデータをフェッチし、ローディング状態やエラー状態を表示する必要があるとします。このロジックを Composable (useFetch) として切り出しておけば、その Composable をインポートして呼び出すだけで、どのコンポーネントでも同じデータフェッチ機能を実装できます。fetch URL など、必要な情報は引数として Composable に渡すことができます。

2. コードの整理と可読性の向上

Composition API自体が関心事ごとのロジック整理を可能にしますが、 Composable を使うことで、その整理をさらに進めることができます。

  • setup() 関数の肥大化を防ぐ: コンポーネントが多くの機能を持つ場合、すべてのロジックをsetup()関数内に直接記述すると、関数が長くなり読みにくくなります。各機能を Composable として切り出すことで、setup()関数内は Composable の呼び出しとその戻り値を扱う部分だけになり、非常にスッキリします。
  • 関心事ごとのグループ化: 特定の機能に関連する状態、算出プロパティ、ウォッチャー、ライフサイクルフックがすべて1つの Composable 関数内にまとめられているため、コードを読む際にその機能全体を簡単に把握できます。Options APIのように、関連コードがコンポーネントの各オプションに分散している状態と比較すると、圧倒的に追いやすくなります。

3. テスト容易性

Composable は基本的にプレーンなJavaScript関数です。

  • コンポーネントから分離してテスト可能: Composable 内のロジックは、特定のVueコンポーネントに依存していません。そのため、Vueのテストユーティリティを使わずに、JestやVitestのような標準的なJavaScriptテストフレームワークを使用して、独立してユニットテストを行うことができます。リアクティブな状態の操作や、ライフサイクルフックがトリガーされたときの挙動などを、素早く効率的にテストできます。これは、コンポーネント全体のマウントやレンダリングを伴うコンポーネントテストよりも高速です。

4. 保守性の向上

ロジックが一箇所にカプセル化されているため、機能の変更やバグ修正が容易になります。

  • 一箇所での修正: 特定の Composable 内のロジックにバグが見つかったり、機能改善を行ったりする場合、修正はその Composable ファイル内だけで完結します。その Composable を使用しているすべてのコンポーネントは、自動的に修正されたロジックの恩恵を受けることができます。Mixins のように、複数のMixinやコンポーネントを跨いでの修正や影響範囲の特定に苦労することが減ります。

5. 型安全性の向上 (TypeScriptとの親和性)

Composable は関数のため、引数や戻り値に明確な型を付けることができます。

  • TypeScriptでの型定義: TypeScriptを使用している場合、Composable 関数とその戻り値、引数に型を定義することで、より堅牢で予測可能なコードを書くことができます。Composable の利用者側(コンポーネント側)は、その Composable がどのような型を返すのかをIDEの補完機能などで確認でき、誤った使い方を防ぐことができます。Mixins ではこのような明確な型の定義と検証が難しかったです。

6. 関心事の分離 (Separation of Concerns)

Composable パターンは、「関心事の分離」というソフトウェア設計の重要な原則を強力に推進します。

  • UIロジックとビジネスロジックの分離: コンポーネントは主にUIの表示、ユーザー操作の受付、および Composable から提供されたデータの表示に集中できます。データのフェッチ、検証、状態管理といったビジネスロジックは Composable に委ねられます。これにより、コンポーネントファイル自体は宣言的で分かりやすくなり、UIとロジックの役割分担が明確になります。

これらのメリットにより、Composable は大規模なアプリケーション開発や、複数人での開発において、コードベースの管理、機能追加、バグ修正を効率的に行うための非常に強力なツールとなります。Options API や Mixins の限界を感じていた開発者にとって、Composable はまさに福音と言えるでしょう。

Composableの作り方 (基本的な構造)

Composable の作り方は非常にシンプルです。基本的には、Composition API を使って記述されたロジックを、use プレフィックスを持つ関数として切り出すだけです。

基本的な構造は以下のようになります。

“`javascript
// useSomething.js または use-something.js
import { ref, reactive, computed, watch, onMounted, onUnmounted, / …その他のComposition API関数 / } from ‘vue’;

// 慣習として ‘use’ プレフィックスを付ける
export function useSomething(/ 必要に応じて引数を受け取る /) {
// 1. リアクティブな状態を宣言
const state = ref(/ 初期値 /);
const anotherState = reactive({ // });

// 2. 算出プロパティを定義 (オプション)
const derivedState = computed(() => {
// state や anotherState を基に計算
return / 計算結果 /;
});

// 3. 状態を操作・更新する関数を定義 (オプション)
function doSomething(//) {
// state や anotherState を変更するロジック
}

// 4. ウォッチャーを定義 (オプション)
watch(state, (newValue, oldValue) => {
// state の変更に対する処理
});

// 5. ライフサイクルのフックを登録 (オプション)
onMounted(() => {
// コンポーネントがマウントされたときの処理
// イベントリスナーの登録など
});

onUnmounted(() => {
// コンポーネントがアンマウントされたときの処理
// イベントリスナーの解除など
});

// 6. 外部に公開したい状態、算出プロパティ、関数を返す
return {
state,
derivedState,
doSomething,
// …その他
};
}
“`

各ステップの詳細:

  1. リアクティブな状態を宣言: Composable が管理する状態は、refreactive を使って宣言します。これにより、これらの状態が変更されたときに、 Composable を使用しているコンポーネントのテンプレートが自動的に更新されるようになります。
  2. 算出プロパティを定義: 既存のリアクティブな状態から別の値を計算して得たい場合は、computed を使用します。computed プロパティもリアクティブなので、依存する状態が変更されると自動的に再計算されます。
  3. 状態を操作・更新する関数を定義: Composable が管理する状態を変更するためのロジックは、関数として定義します。これらの関数は Composable の利用者(コンポーネント)から呼び出せるように、戻り値に含めます。
  4. ウォッチャーを定義: 特定の状態の変更を監視し、副作用を実行したい場合は watch または watchEffect を使用します。
  5. ライフサイクルのフックを登録: Composable が DOM に関連する処理を行ったり、外部のリソース(イベントリスナー、タイマーなど)を管理したりする場合、onMounted, onUnmounted などのライフサイクルフックを使用します。これにより、 Composable を利用しているコンポーネントのライフサイクルに合わせて、セットアップ処理やクリーンアップ処理を適切に実行できます。非常に重要: ライフサイクルフックは、コンポーネントの setup() 関数が実行されている最中にのみ呼び出す必要があります。Composable 関数は、setup() 関数内で呼び出されるため、その内部で安全にライフサイクルフックを使用できます。Composable が setup() の外で(例えばクリックイベントハンドラーの中で)呼び出された場合、ライフサイクルフックは機能しません。
  6. 戻り値: Composable を使用するコンポーネントが利用する必要がある、リアクティブな状態、算出プロパティ、そして状態を操作する関数などを、オブジェクトとして返します。慣習として、戻り値のプロパティ名は Composable 内の変数名と同じにすることが多いですが、必須ではありません。受け取る側で分かりやすい名前に変更することも可能です。

重要なポイント:

  • Composable 関数は、常にコンポーネントの setup() 関数内(または別の Composable 内)で呼び出す必要があります。これにより、Vue のリアクティブシステムやライフサイクルに正しくフックできます。
  • Composable は状態フルなロジックをカプセル化します。つまり、呼び出されるたびに新しい状態インスタンスを持つのが基本です。もし複数のコンポーネントで同じ状態を共有したい場合は、別のパターン(例: Vuex/Piniaのような状態管理ライブラリ、あるいはWritable ComposableやProvider/Injectと組み合わせたパターン)を検討する必要があります。
  • Composable の戻り値は、分割代入 (const { state, doSomething } = useSomething(...)) を使って受け取るのが一般的です。これにより、必要なプロパティだけを簡単に取り出し、名前の衝突を避けることができます。

この基本的な構造を理解した上で、具体的なコード例を通して Composable の実装と使い方を見ていきましょう。

具体的なコード例で学ぶ

ここでは、いくつかの典型的なユースケースを通して、Composable の具体的な実装方法と、それを使うことでどれだけコードがシンプルになるのかを見ていきます。

例1: カウンター機能 (useCounter)

最もシンプルで基本的な Composable の例です。数値の状態を管理し、それをインクリメント・デクリメントする機能を提供します。

Options API での実装例 (比較のため)

“`vue

“`

この例はOptions APIでシンプルに記述できており、特に問題はありません。しかし、もしこの「カウンター」ロジックを複数のコンポーネントで再利用したい場合、Options APIではMixinsを使うか、コードをコピー&ペーストするしかありません。Mixinを使った場合、名前衝突(例えば他のMixinやコンポーネント自身もcountincrementという名前を使う可能性)や追跡困難といった問題が生じる可能性があります。

Composable (useCounter) での実装

カウンターロジックを Composable として切り出します。

“`javascript
// composables/useCounter.js
import { ref } from ‘vue’;

/*
* シンプルなカウンター機能を提供する Composable
* @param {number} initialValue – カウンターの初期値 (省略可能, デフォルトは 0)
* @returns {{ count: Ref, increment: Function, decrement: Function }}
/
export function useCounter(initialValue = 0) {
// 1. リアクティブな状態を宣言
const count = ref(initialValue);

// 2. 状態を操作する関数を定義
function increment() {
count.value++;
}

function decrement() {
count.value–;
}

// 3. 外部に公開したい状態と関数を返す
return {
count,
increment,
decrement
};
}
“`

  • ref(initialValue) でリアクティブな数値状態 count を宣言しています。initialValue を引数で受け取ることで、Composable の呼び出し元から初期値を指定できるようにしています。デフォルト値 0 を設定しています。
  • incrementdecrement 関数は、それぞれ count.value を操作して状態を更新します。
  • 最後に、countref オブジェクトと increment, decrement 関数をオブジェクトとして返しています。

コンポーネント (CounterComponent.vue) での使用例

この Composable を使って、カウンター機能を持つコンポーネントを作成します。

“`vue

“`

  • <script setup> を使用しているので、setup() 関数を明示的に定義する必要はありません。トップレベルのコードが setup() 関数として実行されます。
  • import { useCounter } from '../composables/useCounter'; で Composable をインポートします。
  • const { count, increment, decrement } = useCounter(); で Composable を呼び出し、返された countincrementdecrement を分割代入で受け取ります。これらの変数は、このコンポーネントのテンプレートから直接参照できるようになります。count はリアクティブな ref オブジェクトなので、テンプレートで .value を付けずに参照できます。
  • isEven は、この Composable とは関係なく、このコンポーネント固有の算出プロパティとして定義されています。Composable とコンポーネント固有のロジックを組み合わせるのが簡単であることが分かります。

メリットの解説:

  • シンプルさ: Composable 関数は非常にシンプルで、カウンターロジックだけを含んでいます。
  • 再利用性: useCounter() 関数を他のコンポーネントで呼び出すだけで、同じカウンター機能を簡単に実装できます。各呼び出しは独立したcount状態を持つため、複数のコンポーネントで同時にカウンターを使用しても互いに干渉しません。
  • Options APIからの分離: Options API の Mixin で懸念された名前衝突の問題は、分割代入で受け取る際に変数名を自由に決められるため発生しにくいです。また、useCounter 関数を読めば、何が提供されるのかが明確です。

例2: マウス座標の追跡 (useMousePosition)

ウィンドウ全体のマウス座標を追跡し、その座標状態を提供する Composable です。ライフサイクルフックの使用方法の良い例です。

Composable (useMousePosition) での実装

“`javascript
// composables/useMousePosition.js
import { ref, onMounted, onUnmounted } from ‘vue’;

/*
* マウス座標を追跡する Composable
* @returns {{ x: Ref, y: Ref }}
/
export function useMousePosition() {
// 1. リアクティブな状態を宣言
const x = ref(0);
const y = ref(0);

// 2. イベントハンドラー関数を定義
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}

// 3. ライフサイクルフックを使ってイベントリスナーを管理
// コンポーネントがマウントされたときにリスナーを追加
onMounted(() => {
window.addEventListener(‘mousemove’, update);
console.log(‘Mousemove listener added!’); // デバッグ用
});

// コンポーネントがアンマウントされたときにリスナーを解除
onUnmounted(() => {
window.removeEventListener(‘mousemove’, update);
console.log(‘Mousemove listener removed!’); // デバッグ用
});

// 4. 外部に公開したい状態を返す
return { x, y };
}
“`

  • xy という ref を定義し、初期値は 0 です。
  • update 関数は、マウスイベントオブジェクトから座標を取得し、xy の値を更新します。
  • onMounted フック内で window オブジェクトに mousemove イベントリスナーを登録します。このリスナーが発火すると update 関数が呼び出されます。onMounted は、Composable を呼び出しているコンポーネントがマウントされたときに実行されます。
  • onUnmounted フック内で、登録したイベントリスナーを解除します。これは、コンポーネントがアンマウントされたときに実行され、メモリリークを防ぐために非常に重要です。
  • xyref をオブジェクトとして返します。

コンポーネント (MouseTracker.vue) での使用例

“`vue

“`

  • コンポーネントは useMousePosition を呼び出し、返された xy を表示しています。
  • イベントリスナーの登録や解除といった煩雑なライフサイクル管理ロジックは、すべて useMousePosition Composable 内にカプセル化されているため、コンポーネント自身は非常にシンプルです。

メリットの解説:

  • ライフサイクル管理の容易さ: イベントリスナーの追加と削除という、密接に関連するロジックが onMountedonUnmounted フックを使って Composable 内にまとめられています。これにより、コンポーネント側はそれらの詳細を知る必要がありません。
  • 明確な関心事の分離: マウス座標追跡という特定の機能に必要なすべての要素(状態、イベント処理、ライフサイクル管理)が Composable 内にまとまっています。
  • 再利用性: 他のコンポーネントでもマウス座標を使いたい場合(例えば、カスタムカーソルや要素のドラッグ機能など)、同じ useMousePosition Composable を簡単に再利用できます。

例3: 非同期データのフェッチ (useFetch)

外部APIからデータをフェッチし、ローディング状態、エラー状態、そして取得したデータを管理する Composable です。非同期処理と状態管理を組み合わせた、より実践的な例です。

Composable (useFetch) での実装

“`javascript
// composables/useFetch.js
import { ref, watchEffect } from ‘vue’;

/*
* データを非同期にフェッチする Composable
* @param {string | Ref} url – フェッチする URL (文字列または Ref)
* @returns {{ data: Ref, error: Ref, loading: Ref }}
/
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true); // フェッチ開始時はローディング状態

async function doFetch() {
// 状態をリセット
data.value = null;
error.value = null;
loading.value = true;

try {
  // URL が Ref の場合、.value で値を取得
  const urlValue = typeof url === 'string' ? url : url.value;
  if (!urlValue) { // URL が空の場合は何もしない
     loading.value = false;
     return;
  }

  console.log(`Fetching data from: ${urlValue}`); // デバッグ用
  const res = await fetch(urlValue);

  // HTTP エラー (例: 404 Not Found)
  if (!res.ok) {
    throw new Error(`HTTP error! status: ${res.status}`);
  }

  const result = await res.json();
  data.value = result; // 成功
} catch (err) {
  error.value = err; // エラーが発生
  console.error('Fetch error:', err); // デバッグ用
} finally {
  loading.value = false; // フェッチ完了 (成功/失敗に関わらず)
}

}

// watchEffect を使用して、url の変更を監視し、変更があればフェッチを再実行
// url が Ref の場合、その値の変更を監視できる
// watchEffect は初期実行も行う
watchEffect(() => {
doFetch();
});

// 外部に公開したい状態を返す
return { data, error, loading };
}
“`

  • data, error, loading という3つの ref を定義し、それぞれ取得したデータ、発生したエラー、フェッチ中かどうかを表します。loading は初期値を true にしています。
  • doFetch 関数は、非同期でデータを取得する実際のロジックを含みます。try...catch...finally ブロックを使って、エラーハンドリングとローディング状態の適切な更新を行います。URL が空の場合の処理も追加しています。
  • watchEffect(() => { doFetch(); }) は、Composable の非常に強力な使い方を示しています。watchEffect は、内部で参照しているリアクティブな依存関係(この場合は urlref の場合、その ref の値)が変更されるたびに、その副作用関数を再実行します。また、初期実行も行われます。これにより、 Composable を呼び出すコンポーネント側で urlref を変更すると、自動的にデータが再フェッチされるようになります。これは、APIパラメータが変わるたびにデータを更新したい、といったユースケースで非常に便利です。
  • 最後に、data, error, loadingref を返します。

コンポーネント (DataDisplay.vue) での使用例

“`vue

“`

  • フェッチするURLを currentUrl という ref として定義しています。
  • useFetch(currentUrl) として、URL を ref のまま Composable に渡しています。これにより、currentUrl.value が変更されると、useFetch 内部の watchEffect が検知し、doFetch が自動的に再実行されます。
  • ボタンクリック時に fetchAnotherData 関数が currentUrl.value を更新すると、データが自動的に再フェッチされ、UIが更新されます。
  • コンポーネントのテンプレートでは、loading, error, data といった Composable から提供された状態を使って、条件付きレンダリングを行っています。データフェッチの複雑なロジックは Composable に隠蔽されており、コンポーネントは非常に宣言的です。

メリットの解説:

  • 非同期ロジックの共通化: データのフェッチ、ローディング、エラーハンドリングといった一連の非同期処理ロジックを Composable にまとめることで、他のコンポーネントでも簡単に再利用できます。
  • 状態管理の自動化: ローディング状態やエラー状態が Composable 内で適切に管理・更新されるため、コンポーネント側で手動でこれらの状態を更新する必要がありません。
  • リアクティブな入力: watchEffect を使うことで、 Composable に渡されるリアクティブな引数(この例では URL の ref)の変更を検知し、副作用(再フェッチ)を自動的に実行できます。これは、Options APIでは実現が難しかった、あるいはより多くの定型コードが必要だった機能です。

例4: フォーム入力の検証 (useFormValidation)

複数のフォーム入力フィールドの状態と、それらの検証ロジックを管理する Composable です。複雑なビジネスロジックの分離に役立ちます。

Composable (useFormValidation) での実装

この Composable は、いくつかの入力フィールドの状態と、それに対する簡単な検証ルールを受け取り、入力値、エラーメッセージ、フォーム全体の有効性を返します。

“`javascript
// composables/useFormValidation.js
import { reactive, computed, ref } from ‘vue’;

/*
* フォーム入力と検証を管理する Composable
* @param {object} initialForm – 初期フォームデータ { [fieldName]: initialValue }
* @param {object} rules – 検証ルール { [fieldName]: ruleFunction | Array }
* ruleFunction: (value, form) => string | null (エラーメッセージ文字列または null)
* @returns {{ form: object, errors: object, isFormValid: Ref, resetForm: Function }}
/
export function useFormValidation(initialForm, rules) {
// 1. フォームの状態をリアクティブに宣言
const form = reactive({ …initialForm });

// 2. 各フィールドのエラー状態を算出プロパティとして定義
// computed は依存するリアクティブな値 (form のプロパティ) が変更されるたびに再評価される
const errors = computed(() => {
const errorMessages = {};
for (const field in rules) {
const fieldRules = Array.isArray(rules[field]) ? rules[field] : [rules[field]];
for (const rule of fieldRules) {
// ルール関数を実行し、エラーメッセージを取得
const errorMessage = rule(form[field], form);
if (errorMessage) {
errorMessages[field] = errorMessage;
break; // 最初に見つかったエラーで停止
}
}
// エラーがなければ undefined のまま (テンプレートで扱えるように)
if (!errorMessages[field]) {
errorMessages[field] = undefined;
}
}
console.log(‘Validation errors:’, errorMessages); // デバッグ用
return errorMessages;
});

// 3. フォーム全体の有効性を算出プロパティとして定義
const isFormValid = computed(() => {
// errors オブジェクトに undefined でないプロパティ (エラーメッセージ) が一つでもあれば無効
const hasErrors = Object.values(errors.value).some(message => message !== undefined);
// 全ての必須フィールドが入力されているかもチェックが必要な場合はここに追加
// 例: const allFieldsFilled = Object.keys(initialForm).every(field => form[field] !== ” && form[field] !== null && form[field] !== undefined);
// return !hasErrors && allFieldsFilled;
return !hasErrors; // シンプルにエラーメッセージがないことだけをチェック
});

// 4. フォームを初期状態にリセットする関数
function resetForm() {
Object.assign(form, initialForm);
}

// 5. 外部に公開したい状態と関数を返す
// form は reactive オブジェクト自体を返す
// errors と isFormValid は computed Ref を返す
// resetForm は通常の関数を返す
return {
form,
errors,
isFormValid,
resetForm,
};
}
“`

  • initialFormrules という2つの引数を受け取ります。initialForm はフォームの初期データ構造、rules はフィールドごとの検証ルールを定義したオブジェクトです。
  • formreactive オブジェクトとして定義され、フォームの現在の入力値を保持します。
  • errorscomputed プロパティです。form オブジェクトのプロパティが変更されるたびに再計算され、各フィールドに対する検証ルールを実行し、エラーメッセージを持つオブジェクトを生成します。
  • isFormValidcomputed プロパティで、errors オブジェクトの内容を見て、フォーム全体が有効かどうかを判定します。
  • resetForm 関数は、forminitialForm の値に戻します。
  • formerrorsisFormValidresetForm を戻り値として返します。

コンポーネント (FormComponent.vue) での使用例

“`vue

“`

  • initialFormDatavalidationRules を定義し、それらを useFormValidation Composable に渡しています。
  • Composable から返された form, errors, isFormValid, resetForm を受け取ります。
  • テンプレートでは v-model="form.fieldName" を使って入力フィールドと form オブジェクトをバインドしています。
  • v-if="errors.fieldName" で、対応するフィールドのエラーメッセージが表示されます。
  • 送信ボタンは :disabled="!isFormValid" とバインドされており、フォームが有効でない場合は無効になります。
  • handleSubmit 関数では isFormValid.value をチェックして、フォームが有効な場合のみ送信処理を行います。

メリットの解説:

  • 複雑なロジックの分離: フォームの状態管理と検証という、UIとは直接関係ないビジネスロジックを Composable に完全に分離できています。
  • 再利用性: useFormValidation Composable は、他のフォームコンポーネントでも簡単に再利用できます。初期データとルールを渡すだけで、検証機能付きのフォームを実装できます。
  • 宣言的なテンプレート: コンポーネントのテンプレートは、 Composable から提供された状態(form, errors, isFormValid)を単に表示し、ユーザーインタラクション(入力、ボタンクリック)を処理するだけになり、非常に宣言的で読みやすくなります。
  • 柔軟なルール定義: 検証ルールをオブジェクトとして Composable に渡すことで、 Composable 自体を変更することなく、様々なフォームやフィールドに対して異なる検証ルールを適用できます。

例5: 状態管理 (Vuex/Piniaを使わない簡単な例)

複数のコンポーネント間でシンプルに状態を共有したい場合の Composable の使い方です。大規模な状態管理には Vuex や Pinia が推奨されますが、ごく小規模なアプリケーションや、特定の限られた状態だけを共有したい場合には、 Composable と Provide/Inject を組み合わせる方法も有効です。

共有状態を管理する Composable (useSharedState)

この Composable は、共有される状態と、その状態を変更するための関数を提供します。状態自体をシングルトンとして持つために、 Composable の定義の外側で状態を宣言します。

“`javascript
// composables/useSharedState.js
import { ref } from ‘vue’;

// Composable 関数の外側で状態を宣言することで、全ての呼び出し元で同じ状態インスタンスを共有できる (シングルトンパターン)
const sharedCounter = ref(0);
const sharedMessage = ref(‘Hello Shared State’);

/*
* 複数のコンポーネント間で共有される状態と、それを操作する関数を提供する Composable
* 状態は全ての Composable 利用者で共有されるシングルトンです。
* @returns {{ sharedCounter: Ref, sharedMessage: Ref, incrementSharedCounter: Function, setSharedMessage: Function }}
/
export function useSharedState() {
// 状態を操作する関数
function incrementSharedCounter() {
sharedCounter.value++;
}

function setSharedMessage(newMessage) {
sharedMessage.value = newMessage;
}

// 共有状態と操作関数を返す
return {
sharedCounter,
sharedMessage,
incrementSharedCounter,
setSharedMessage,
};
}
“`

  • sharedCountersharedMessage は、 Composable 関数の外側ref として宣言されています。これにより、このモジュールがインポートされるたびに同じ ref オブジェクトが再利用され、結果として useSharedState を呼び出す全てのコンポーネントが同じ状態インスタンスを参照することになります(シングルトン)。
  • incrementSharedCountersetSharedMessage 関数は、これらの共有状態を操作します。
  • これらの状態と関数を戻り値として提供します。

コンポーネント A (ComponentA.vue) での使用例

“`vue

“`

コンポーネント B (ComponentB.vue) での使用例

“`vue