【Vue】コンポーザブル(Composable)を自作してロジックを再利用する

JavaScript

こんにちは、かつコーチです。

前回はライフサイクルフック(onMounted など)の使い方を解説しました。

今回は、Composition API編の締めくくりとして「コンポーザブル(Composable)」を扱います。

同じようなロジックを何個も違うコンポーネントにコピペしていませんか。

コンポーザブルを使えると、そのコピペから卒業できます。

コンポーザブルとは何か

「使い回せるロジックの塊」という考え方

コンポーザブルとは、refcomputed、ライフサイクルフックなどを組み合わせて作る、再利用可能な関数のことです。

慣習として useXxx という名前を付けます(useCounteruseFetch など)。

Reactのカスタムフックに近い考え方ですが、Vueでは特別な決まりごとが少なく、ただの「リアクティブな値を返す関数」として作れます。

コンポーネントにロジックが増えていく問題

コンポーザブルを使わない場合、似たようなロジックがコンポーネントごとにベタ書きされていきます。

たとえば「ウィンドウ幅を監視してスマホ表示かどうかを判定する」処理を、3つのコンポーネントでそれぞれ書いていたら、修正のたびに3箇所を直す必要が出てきます。

私が実際につまずいたのは、この「同じロジックの複製」に気づかず開発を進めてしまったケースでした。

レスポンシブ判定のロジックをコンポーネントごとにコピペしていたところ、ブレークポイントの数値を768pxから900pxに変更する際、直し忘れが1箇所発生し、画面によって切り替わりのタイミングがバラバラになるという不具合を出してしまいました。

基本のコンポーザブルを作る

ファイルの置き場所

コンポーザブルは composables/ ディレクトリにまとめるのが一般的です。

src/
  composables/
    useCounter.ts
    useWindowSize.ts
  components/
    Counter.vue

カウンターのコンポーザブル

まずはシンプルな例から見てみましょう。

// composables/useCounter.ts
import { ref } from "vue"

export function useCounter(initialValue = 0) {
  const count = ref<number>(initialValue)

  function increment(): void {
    count.value++
  }

  function decrement(): void {
    count.value--
  }

  function reset(): void {
    count.value = initialValue
  }

  return { count, increment, decrement, reset }
}

コンポーネント側では、次のように呼び出すだけで使えます。

<!-- Counter.vue -->
<script setup lang="ts">
import { useCounter } from "@/composables/useCounter"

const { count, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <p>カウント:{{ count }}</p>
  <button @click="increment">+1</button>
  <button @click="decrement">-1</button>
  <button @click="reset">リセット</button>
</template>

ロジック部分がコンポーネントから完全に切り離されているため、同じ挙動のカウンターを別の画面でも useCounter() を呼ぶだけで再現できます。

実践:ウィンドウ幅を監視するコンポーザブル

ライフサイクルフックと組み合わせる

前回学んだ onMounted / onUnmounted を組み合わせると、より実践的なコンポーザブルが作れます。

// composables/useWindowSize.ts
import { ref, onMounted, onUnmounted } from "vue"

export function useWindowSize() {
  const width = ref<number>(window.innerWidth)
  const height = ref<number>(window.innerHeight)

  function handleResize(): void {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }

  onMounted(() => {
    window.addEventListener("resize", handleResize)
  })

  onUnmounted(() => {
    window.removeEventListener("resize", handleResize)
  })

  return { width, height }
}

イベントリスナーの登録・解除がコンポーザブルの内部に閉じ込められているので、呼び出す側はそうした後片付けの心配をする必要がありません。

<script setup lang="ts">
import { computed } from "vue"
import { useWindowSize } from "@/composables/useWindowSize"

const { width } = useWindowSize()
const isMobile = computed<boolean>(() => width.value < 768)
</script>

<template>
  <p v-if="isMobile">スマホ表示です</p>
  <p v-else>PC表示です</p>
</template>

ブレークポイントを変更したいときも、isMobile を使っているコンポーネント側の1行を直すだけで済みます。

つまずきやすいポイント:リアクティビティの喪失

分割代入で ref が壊れるケース

コンポーザブルを使ううえで、多くの人がハマるのがこのパターンです。

❌ Before:戻り値を分割代入で普通の変数として受け取る

// composables/useCounter.ts
import { ref } from "vue"

export function useCounter() {
  const count = ref<number>(0)
  return { count }
}
<script setup lang="ts">
import { useCounter } from "@/composables/useCounter"

const { count } = useCounter()
const currentCount = count.value // ここで値だけを取り出してしまう

// currentCountはただの数値になり、以後countが変化しても更新されない
</script>

ref から .value を一度取り出してしまうと、そこでリアクティビティ(値の変化を追跡する性質)が切れてしまいます。

{ count } のように ref オブジェクトのまま受け渡す分には問題ないのですが、そこからさらに .value を取り出して別の変数に代入すると、その時点のスナップショットになってしまうのです。

✅ After:refのまま扱い、必要な場所でだけ.valueにアクセスする

<script setup lang="ts">
import { useCounter } from "@/composables/useCounter"

const { count } = useCounter()
// countはrefのまま。テンプレートでは自動的にアンラップされる
</script>

<template>
  <p>カウント:{{ count }}</p>
</template>

コンポーザブルの戻り値は、基本的に refcomputed のまま扱い、テンプレートに直接バインドするのが安全です。

途中で .value を取り出して別の変数に固定してしまわないよう注意しましょう。

まとめ

この記事のポイント

  • コンポーザブルは、ref やライフサイクルフックをまとめた再利用可能な関数
  • composables/useXxx.ts という命名・配置が慣習になっている
  • イベントリスナーの登録・解除など「対になる処理」をコンポーザブル内に閉じ込めると、呼び出し側がシンプルになる
  • 戻り値の ref から .value を早い段階で取り出すと、リアクティビティが失われるので注意する

次に読むべき記事

これでComposition APIの基本〜応用は一通り押さえられました。

次回からは、アプリ全体で状態を管理するための「Pinia」を扱っていきます。

→ 次の記事:Pinia入門:Vueの状態管理ライブラリ

タイトルとURLをコピーしました