Composables - 状態を持つロジックの再利用
ロードマップ: Vue.js学習ロードマップ
What(何についてか)
Composable は、Vue の Composition API を使って stateful logic をカプセル化し、再利用する関数 である。
単なる utility 関数と異なり、内部で ref() による状態管理、watchEffect() による反応、onMounted() / onUnmounted() による副作用の開始と破棄を扱える。
代表的な用途は以下である。
- ブラウザイベント購読(例: マウス座標、画面サイズ、オンライン状態)
- 非同期データ取得(loading / success / error 状態管理)
- フォームやバリデーションの状態管理
- 複数コンポーネントで繰り返される UI 非依存ロジックの共通化
Why(なぜ必要か)
コンポーネントに状態変化と副作用を含むロジックを直接書き続けると、UI 記述と振る舞い記述が混ざり、見通しが悪くなる。 Composable を使うと、ロジックを「意味のある振る舞い単位」でコンポーネント外へ抽出できる。
この方式の利点は以下である。
- コンポーネントを UI 表現に集中させられる
- 同じ stateful logic を複数箇所で再利用できる
- 責務の分離により、変更時の影響範囲を狭められる
- 小さな composable を組み合わせて大きな振る舞いを構成できる
Vue 3 において composable が推奨される理由は、過去の mixins よりも依存関係が明示的で、renderless component よりも軽量にロジックを共有できるためである。
How(どう動くか)
基本構造
Composable は慣習的に useXxx という名前を持つ。
内部で状態を作り、必要に応じて副作用を登録し、外に公開したい値だけを返す。
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
const increment = () => {
count.value += 1
}
return { count, increment }
}呼び出し側は <script setup> または setup() で使う。
<script setup lang="ts">
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>Mouse Tracker 例
マウス座標追跡のように、時間とともに変化する状態と DOM イベント購読は composable に向いている。
import { ref, onMounted, onUnmounted } from 'vue'
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 }
}この例の要点は、状態管理・イベント購読・クリーンアップを 1 つの API に閉じ込めている点にある。
呼び出し側コンポーネントは x と y を受け取るだけでよい。
Composable のネスト
Composable は他の composable を内部で利用できる。 これにより、ロジックを責務ごとにさらに細かく分解できる。
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(
target: Window,
event: string,
callback: EventListener
) {
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
const mouseEvent = event as MouseEvent
x.value = mouseEvent.pageX
y.value = mouseEvent.pageY
})
return { x, y }
}この分割により、useEventListener() はイベント購読責務だけを担当し、useMouse() はマウス座標という意味を持つ状態だけを担当する。
非同期状態管理
Composable は API fetch のような非同期処理にも適用できる。 重要なのは、単なる fetch 呼び出しではなく、loading / success / error という 状態遷移パターン を再利用する点である。
import { ref, watchEffect, toValue, type Ref } from 'vue'
type MaybeRefOrGetter<T> = T | Ref<T> | (() => T)
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(true)
const fetchData = async () => {
data.value = null
error.value = null
loading.value = true
try {
const response = await fetch(toValue(url))
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = (await response.json()) as T
} catch (err) {
error.value = err instanceof Error ? err : new Error(String(err))
} finally {
loading.value = false
}
}
watchEffect(() => {
fetchData()
})
return { data, error, loading }
}toValue() を使うことで、文字列・Ref<string>・getter 関数のいずれでも受けられる。
この設計により、props.id や他の reactive state の変化に応じて composable 側の副作用を再実行できる。
設計上の重要原則
返り値は plain object + refs
Composable の返り値は、reactive() で包んだオブジェクトではなく、plain object に refs を載せて返す設計が推奨される。
return { x, y, loading, error }この形であれば呼び出し側で分割代入しても reactivity を失わない。
const { x, y } = useMouse()一方で、reactive() オブジェクトを返して分割代入すると、各プロパティは単なる値になり、reactive な接続が切れる可能性がある。
reactive input を受けられるようにする
Composable の引数を固定値だけにすると、再利用性が低い。 実務では以下をそのまま受け取れるように設計すると使いやすい。
- 生の値
ref- getter 関数
toValue() や getter パターンを使うと、呼び出し側のコンポーネントで不要な watcher を増やさずに済む。
setup 文脈で同期的に呼ぶ
Composable は通常の関数のように見えるが、内部で Vue の lifecycle hook や watcher を使う場合、コンポーネントの setup 文脈で同期的に呼ばれる必要がある。
安全な呼び出し場所は以下である。
<script setup>のトップレベルsetup()関数の同期処理中
setTimeout() やイベントハンドラの中で初めて lifecycle 付き composable を呼ぶ設計は避ける。
所有コンポーネントとの紐付けや cleanup が不明瞭になるためである。
Composition API における位置づけ
Composable は、Vue 3 におけるロジック再利用の第一選択肢である。 ただし、再利用したい対象によって使い分ける。
flowchart TD A["再利用したい対象は何か"] --> B["UI レイアウトも含む"] A --> C["ロジックだけ"] A --> D["アプリ全体で共有する状態"] B --> E["Component を使う"] C --> F["Composable を使う"] D --> G["State Management を検討する"]
Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| ロジック再利用 | mixins が中心 | composable が中心 |
| 値の由来 | instance に暗黙注入されやすい | useXxx() の返り値として明示的 |
| 名前衝突 | mixin 間で起こりやすい | 分割代入時に rename 可能 |
| ロジック合成 | 暗黙結合しやすい | 関数呼び出しで明示的に合成 |
| Setup 文脈 | 不要 | composable は setup() / <script setup> 内で使う |
他手法との比較
vs Mixins
mixins は便利だが、値の出どころが不明瞭になりやすく、namespace collision や暗黙の結合が起きやすい。 Composable は通常の関数として呼び出し、返り値を明示的に受け取るため、依存関係が追いやすい。
vs Renderless Components
Renderless Component は slot を使ってロジックを共有するが、追加のコンポーネントインスタンスが必要になる。 Composable はロジックのみを共有したい場合に、より軽量である。
vs React Hooks
命名や発想は似ているが、Vue composable は Vue の fine-grained reactivity を前提としている。 React Hooks の再実行モデルと見た目が似ていても、依存追跡と更新の粒度は異なる。
Key Concepts
| 用語 | 説明 |
|---|---|
| Composable | Composition API を使って stateful logic を再利用する関数 |
| Stateful Logic | 時間経過やイベントによって変化する状態を含むロジック |
useXxx | composable であることを示す慣習的な命名 |
| Reactive Input | ref や getter のように変化を追跡できる入力 |
toValue() | 値 / ref / getter を統一的に値へ正規化する API |
watchEffect() | 内部で参照した reactive dependency を自動追跡して再実行する API |
| Plain Object Return | refs をそのまま返し、分割代入しても reactivity を保てる返却方式 |
| Composable Nesting | composable の中から別の composable を呼び、責務を細分化して再合成する設計 |
実務での判断軸
- 画面の見た目と振る舞いをまとめて再利用したいなら component を使う
- 状態を持つロジックだけを切り出したいなら composable を使う
- 複数画面やアプリ全体で共有する状態が主題なら state management を検討する
Composable は万能ではないが、Vue 3 において「コンポーネントからロジックを剥がす第一手」として非常に強力である。