今回のテーマ
Vue.jsでコンポーネントのロジックを抽出する際、Composableは非常に強力なパターンです。
しかし、「このロジックは1箇所でしか使わないから、Composableにする必要はない」と判断していませんか?
実は、この考え方こそが、Composableの本質を見誤る典型的な罠なのです。
本記事では、Composableの定義から紐解き、なぜ「責任分離」こそが重要なのかを解説します。
Composableの定義とよくある誤解
Vue.js公式ドキュメントでは、Composableを次のように定義しています。
状態を持つロジックをカプセル化して再利用するための関数
この定義において、「再利用」という言葉が強調されるため、多くの開発者が「複数箇所で使われるかどうか」を判断基準にしてしまいます。
よくある思考パターン
「このロジックは今のところこのコンポーネントでしか使わないから、Composableにしなくていいか」
一見合理的に思えるこの判断ですが、実は大きな落とし穴があります。
「再利用しない」の裏にある本当の問題
「再利用しないだろう」と考えてしまう場合、その多くは責任分離が不十分である可能性が高いです。
責任過多が引き起こす問題
責任分離が不十分なロジックは、以下のような特徴を持ちます:
- 特定の場面に最適化されすぎている: 「このコンポーネントの、この状況で使う」ことを前提に設計されている
- 複数の関心事が混在している: データ取得、状態管理、UI制御などが1つのロジックに詰め込まれている
- 暗黙の依存関係がある: コンポーネント固有の状態や外部要因に依存している
結果として、そのロジックは特定のコンテキストでしか機能せず、汎用性が失われます。
そして「やっぱり再利用できなかった」という結論に至るのです。
責任分離がもたらす自然な再利用性
適切に責任分離されたロジックは、自然と再利用可能な形になります。
これは「再利用を目的として設計する」のではなく、「単一責任の原則に従った結果、再利用可能になる」という順序が重要です。
責任分離の実践例
以下は、責任が混在したComposableの例です:
// 悪い例: 責任が混在している
export function useUserProfile() {
const user = ref(null)
const isLoading = ref(false)
const errorMessage = ref('')
const showNotification = ref(false)
async function fetchUser(userId) {
isLoading.value = true
errorMessage.value = ''
try {
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
user.value = {
...data,
displayName: `${data.firstName} ${data.lastName}`, // UI表示用の加工
avatarUrl: data.avatar || '/default-avatar.png' // デフォルト値の設定
}
showNotification.value = true // 通知の表示制御
} catch (error) {
errorMessage.value = 'ユーザー情報の取得に失敗しました'
} finally {
isLoading.value = false
}
}
return { user, isLoading, errorMessage, showNotification, fetchUser }
}
このComposableは、データ取得、データ加工、エラーハンドリング、UI状態管理という複数の責任を持っています。
責任分離後のコード
これを適切に分離すると、以下のようになります:
// 良い例1: データ取得に特化
export function useFetch(url) {
const data = ref(null)
const isLoading = ref(false)
const error = ref(null)
async function execute() {
isLoading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
isLoading.value = false
}
}
return { data, isLoading, error, execute }
}
// 良い例2: ユーザーデータの加工に特化
export function useUserFormatter(user) {
const displayName = computed(() =>
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
)
const avatarUrl = computed(() =>
user.value?.avatar || '/default-avatar.png'
)
return { displayName, avatarUrl }
}
// 良い例3: 通知状態の管理に特化
export function useNotification() {
const isVisible = ref(false)
function show() {
isVisible.value = true
}
function hide() {
isVisible.value = false
}
return { isVisible, show, hide }
}
分離のメリット
責任を分離することで、以下のような利点が得られます:
-
useFetch
: API通信全般で再利用可能 -
useUserFormatter
: ユーザー情報を表示する全ての場所で再利用可能 -
useNotification
: あらゆる通知シーンで再利用可能
それぞれが単一の責任を持つため、予測可能で、テストしやすく、そして自然と再利用可能になっています。
再利用性の本当の意味
ここで重要なのは、「再利用性」を狭く捉えすぎないことです。
再利用性が持つ複数の側面
- コード的な再利用: 複数のコンポーネントで同じComposableを使用する
- テスト容易性: ロジックを独立してテストできる
- 保守性: 変更の影響範囲が明確で、修正しやすい
- 理解しやすさ: 単一責任なので、コードを読む際の認知負荷が低い
たとえ現時点で1箇所でしか使われていないComposableでも、適切に責任分離されていれば、上記のメリットは全て享受できます。
まとめ
Composableを作成する際の判断基準は、「複数箇所で使うかどうか」ではなく、「適切に責任分離されているか」です。
以下は、本記事のポイントです。
- Composableの定義: 「状態を持つロジックをカプセル化して再利用するための関数」
- よくある誤解: 「再利用しないからComposableにしない」という判断は、責任分離の不足を示唆している
- 正しいアプローチ: 単一責任の原則に従ってロジックを分離すると、自然と再利用可能な形になる
- 再利用性の本質: コードの再利用だけでなく、テスト容易性、保守性、理解しやすさも含まれる
「再利用されるかどうか」を気にするのではなく、「このロジックは単一の責任を持っているか」を問いかけることで、結果的にComposableの定義を満たす、質の高いコードが生まれます。