---
title: "Composition APIとは？Options API・React Hooksとの違いとコンポーザブルの書き方【Vue 3.5】"
url: "https://www.issoh.co.jp/tech/details/3589/"
published: 2024-09-13
updated: 2026-11-02
categories: ["Vue.js"]
publisher: "株式会社一創"
---

# Composition APIとは？Options API・React Hooksとの違いとコンポーザブルの書き方【Vue 3.5】

Composition APIは、Vue（ビュー）3でコンポーネントの状態とロジックを関数として組み立てる記述スタイルです。`data` や `methods` といったオプションの箱に振り分けるOptions APIに対して、Composition APIは `ref()` や `computed()` を素のJavaScriptの変数・関数として書き、必要な単位で切り出して再利用します。この記事は2026年11月時点の安定版であるVue 3.5.44（2026年10月13日公開）を前提に、基本の書き方、コンポーザブル関数の作り方と呼び出しの制約、TypeScript連携、Options APIとReact Hooksとの違いまでを実コードで整理します。

## まとめ

Composition APIは `<script setup>` の中でリアクティブな変数と関数を宣言し、それをそのままテンプレートから使う書き方です。Options APIとの間に機能の優劣はなく、公式ドキュメントも「Options APIは Composition API を土台にしています」と説明しています。差が出るのは、ロジックを部品として切り出すときと、TypeScriptで型を通すときです。

選び方は公式の推奨どおりで足ります。アプリケーション全体をVueで構築するならComposition API、ビルドツールを使わない構成や複雑性の低い画面ならOptions APIです。Options APIを廃止する予定はなく、公式FAQの回答は「いいえ、私たちは特にそうする予定はありません」の一文です。

React Hooksとの違いは実行モデルにあります。Hooksは再レンダーのたびに関数全体が再実行されるため依存配列を手で書きますが、Composition APIの `setup` は1回しか実行されず、依存はリアクティブシステムが自動で追跡します。2025年10月にReact Compiler 1.0が出てメモ化の手間は縮みましたが、この実行モデルの差は残っています。

ビルドツールを使ってアプリケーション全体を新規に書くなら、Composition APIを選んでください。Vue 3.6のVapor Modeは `<script setup>` を使うSFCとテンプレートのみのSFCにしか対応せず、Options APIは対象外です。3.6は2026年10月30日公開の3.6.0-rc.14の時点でもRCで、安定版はまだ出ていません。

## Composition APIの定義と基本形

### Options APIとの構造上の違い

同じカウンターを両方の書き方で並べると、違いは「どこに何を置くか」だと分かります。Options APIは値を `data`、派生値を `computed`、処理を `methods`、ライフサイクルを `mounted` という決まった位置に置きます。

```
<script>
export default {
  data() {
    return { count: 0 }
  },
  computed: {
    doubled() { return this.count * 2 }
  },
  methods: {
    increment() { this.count++ }
  },
  mounted() { console.log('mounted') }
}
</script>
```

Composition APIでは、同じ内容が関数スコープ内の変数宣言と関数定義になります。値の置き場所がオプション名で決まらないので、関連する処理をまとめて書けます。

```
<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
onMounted(() => console.log('mounted'))
</script>

<template>
  <button @click="increment">{{ count }} / {{ doubled }}</button>
</template>
```

Composition APIの側には `this` が出てきません。Options APIはコンポーネントインスタンスの `this` を経由して値にアクセスしますが、`<script setup>` のテンプレートは同じスコープ内のインライン関数としてコンパイルされ、インスタンスプロキシを挟まずに変数へ直接アクセスします。[公式のComposition API FAQ](https://ja.vuejs.org/guide/extras/composition-api-faq)は、これによって変数名を安全に短縮でき、縮小化の効きがよくなると説明しています。

### setup関数と script setup の関係

`<script setup>` はコンパイル時の糖衣構文で、実体は `setup()` 関数です。ライブラリのように props の定義を動的に組み立てたい場合や、Options APIのコンポーネントに部分的にComposition APIを持ち込む場合は、`setup()` を直接書きます。

```
<script>
import { ref } from 'vue'

export default {
  props: { initial: Number },
  setup(props, { emit }) {
    const count = ref(props.initial ?? 0)
    function increment() {
      count.value++
      emit('change', count.value)
    }
    // return したものだけがテンプレートから見える
    return { count, increment }
  }
}
</script>
```

`setup()` を直接書く形では、テンプレートに公開したい値を明示的に `return` します。`<script setup>` はトップレベルの束縛が自動的にテンプレートへ公開されるため、記述量が少なく済みます。新規に書くなら `<script setup>` を既定にしてください。

## リアクティブな状態の宣言と監視

### refとreactiveの使い分け

状態の宣言には `ref()` と `reactive()` の2つがあります。`ref()` は数値や文字列を含むあらゆる値を扱え、読み書きは `.value` を経由します。`reactive()` はオブジェクトと配列専用で、`.value` が要らない代わりに、変数ごと再代入したり、数値や文字列のプロパティを分割代入で取り出したりすると参照が切れるという制約を持ちます。

```
import { ref, reactive } from 'vue'

// プリミティブは ref。読み書きは .value 経由
const count = ref(0)
count.value++

// ref にオブジェクトを入れても内部は深いリアクティブになる
const user = ref({ name: 'sato', age: 30 })
user.value.age = 31   // これも検知される

// reactive はオブジェクト専用。.value は不要だが再代入で参照が切れる
const state = reactive({ items: [], loading: false })
state.loading = true
```

迷ったら `ref()` に寄せてください。[公式のリアクティビティーの基礎](https://ja.vuejs.org/guide/essentials/reactivity-fundamentals)も、`reactive()` の制約を理由に `ref()` を主なAPIとして使うことを勧めています。`ref()` に入れたオブジェクトは内部で `reactive()` に変換されるので、ネストしたプロパティの書き換えも検知されます。

### computedとwatchの役割分担

既存の状態から計算できる値は `computed()`、状態の変化をきっかけに外部へ働きかける処理は `watch()` に書きます。この境界を崩すと、計算のたびにAPIを叩くコードや、値の更新が二重に走るコードになります。

```
import { ref, computed, watch, onWatcherCleanup } from 'vue'

const keyword = ref('')
const results = ref([])
const trimmed = computed(() => keyword.value.trim())   // 派生値は computed

watch(trimmed, async (value) => {                      // 副作用は watch
  const controller = new AbortController()
  onWatcherCleanup(() => controller.abort())           // 3.5 以降
  try {
    const params = new URLSearchParams({ q: value })        // & や # を含む語もエンコードする
    const res = await fetch(`/api/search?${params}`, { signal: controller.signal })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    results.value = await res.json()
  } catch (e) {
    if (e.name !== 'AbortError') console.error(e)          // 中断は正常系として無視する
  }
})
```

Vue 3.5で追加された `onWatcherCleanup()` は、監視対象が次に変わったときやコンポーネントの破棄時に、前回の副作用を止めるための関数です。上の例では進行中のリクエストを `AbortController` で中断しています。中断された `fetch` は `AbortError` で失敗するので、`try`/`catch` で受けないと連続入力のたびに未処理の例外が出ます。同じ3.5では `watch` の `deep` に数値を渡して監視の深さを指定できるようになり、ウォッチャーの一時停止と再開にも対応しました。

### リアクティビティが失われる書き方

一番踏みやすい落とし穴は `reactive()` の分割代入です。数値や文字列のようなプリミティブのプロパティを取り出すと、その時点の値がコピーされるだけでリアクティブな参照が切れ、以後の変更を追えなくなります。ネストしたオブジェクトを取り出した場合は中身がリアクティブなプロキシのままなので追跡されますが、この違いを毎回意識するより、取り出す前に `toRefs()` を通すほうが安全です。

```
import { reactive, toRefs } from 'vue'

// NG: 分割代入した時点でただの数値になり、以後の変更を追えない
const a = reactive({ count: 0 })
let { count } = a
a.count++
console.log(count, a.count)       // 0 1

// OK: toRefs で ref に変換してから取り出せば変更に追随する
const b = reactive({ count: 0 })
const { count: countRef } = toRefs(b)
b.count++
console.log(countRef.value)       // 1
```

vue 3.5.44 でこのコードを実行すると、1つ目の `console.log` は `0 1` を出力します。分割代入した `count` は初期値のまま取り残され、元のオブジェクトだけが増えた状態です。`toRefs()` を通した2つ目は `1` で、参照が保たれています。`reactive()` を関数の戻り値やpropsとして受け渡すコードでは、この事故がエラーも出さずに混入します。状態を関数の外へ出すときは `ref()` か `toRefs()` を使ってください。

## Vueのコンポーザブル関数によるロジックの再利用

### コンポーザブルの実装と命名規則

コンポーザブル（composable）は、Composition APIを使って状態を持つロジックを閉じ込めた関数です。名前は `use` で始まるキャメルケースにし、refを個別に詰めたオブジェクトを返します。公式ドキュメントがComposition APIの最大の利点に挙げるのが、このコンポーザブルによるロジック再利用です。なお、Androidの Jetpack Compose にも `@Composable` という同名の仕組みがありますが、Vueのコンポーザブルとは無関係です。

```
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async (onCleanup) => {
    const target = toValue(url)   // ref でも getter でも素の値でも受け取れる
    if (!target) return
    let cancelled = false
    onCleanup(() => { cancelled = true })   // 次の実行や破棄の前に前回分を無効にする
    loading.value = true
    try {
      const json = await (await fetch(target)).json()
      if (!cancelled) { data.value = json; error.value = null }
    } catch (e) {
      if (!cancelled) error.value = e
    } finally {
      if (!cancelled) loading.value = false
    }
  })

  return { data, error, loading }
}
```

引数を `toValue()` で受けると、呼び出し側は素の値・ref・getter関数のどれでも渡せます。getterで渡せば依存が追跡され、引数が変わると中の `watchEffect` が再実行されます。`onCleanup` で前回の実行を無効にしているのは、URLが変わった直後に古いリクエストが遅れて返り、新しい `data` を上書きするのを防ぐためです。この処理を省いた版をvue 3.5.44で動かし、新しいリクエストが先に、古いリクエストが後に返るようにすると、`data` は古いURLの結果に戻りました。

```
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const id = ref(1)
// 引数を getter で渡すと id の変更に追随して再取得される
const { data, error, loading } = useFetch(() => `/api/users/${id.value}`)
</script>
```

戻り値を分割代入できるのは、コンポーザブルがrefのオブジェクトを返しているからです。refを個別に返す形が推奨されるのは、利用側でどのプロパティがどのコンポーザブル由来かが読めるためです。状態をアプリケーション全体で共有する場合、小規模なら自前のリアクティブな状態で足ります。SSR対応、Vue DevToolsでのタイムトラベルデバッグ、チームで揃える規約が必要になった段階で、[Vue 3対応の状態管理ライブラリであるPinia](/tech/details/3513/)に移してください。公式の状態管理ガイドも、新しいアプリケーションにはPiniaを勧めています。

### 呼び出せる場所の制約と副作用の後始末

[公式ドキュメントのコンポーザブルの章](https://ja.vuejs.org/guide/reusability/composables)は、コンポーザブルを `<script setup>` か `setup()` の中で同期的に呼ぶよう求めています。Vueはその時点で実行中のコンポーネントを手がかりに、ライフサイクルフックやウォッチャーを登録するからです。例外は2つあります。`onMounted()` のようなライフサイクルフックの中で呼ぶ場合と、`<script setup>` のトップレベルに書いた `await` の後で呼ぶ場合です。後者はコンパイラが非同期処理のあとにコンポーネントの文脈を復元してくれるためで、自分で定義した `async` 関数の中の `await` の後では復元されません。

```
// composables/useEventListener.js
import { onMounted, onUnmounted, toValue } from 'vue'

export function useEventListener(target, event, handler) {
  // DOM に触る副作用はマウント後に始め、アンマウントで必ず外す
  onMounted(() => toValue(target).addEventListener(event, handler))
  onUnmounted(() => toValue(target).removeEventListener(event, handler))
}
```

```
<script setup>
import { useEventListener } from '@/composables/useEventListener'

const onResize = () => console.log(window.innerWidth)

// OK: setup の中で同期的に呼ぶ。window は getter で渡し、評価をマウント後まで遅らせる
useEventListener(() => window, 'resize', onResize)

// NG: タイマーやイベントハンドラーの中では、どのコンポーネントの処理か特定できない
setTimeout(() => useEventListener(window, 'resize', onResize), 0)
</script>
```

NG側をvue 3.5.44の開発ビルドで動かすと、`[Vue warn]: onMounted is called when there is no active component instance to be associated with.` という警告が出て、登録したつもりの `onMounted` と `onUnmounted` は一度も実行されません。警告だけで処理は止まらないので、リスナーが付かない不具合として本番に紛れ込みます。SSRを使う構成では、DOMに触る副作用を `onMounted()` の中に置くことも必須です。マウント後のフックはブラウザでしか呼ばれません。ただし引数に `window` をそのまま書くと、その引数はサーバーでの `setup` 実行時に評価され、`ReferenceError: window is not defined` で落ちます（vue 3.5.44のサーバーレンダラーで確認）。上の例のようにgetterで渡し、コンポーザブル側で `toValue()` を使ってマウント後に取り出してください。

### ミックスインの3つの欠点

Vue 2でロジックを共有する手段はミックスインでした。公式ドキュメントはミックスインに3つの主要な欠点があるとし、それぞれをコンポーザブルがどう解決するかを示しています。

| ミックスインの欠点  | 中身                  | コンポーザブルでの解決     |
| ---------- | ------------------- | --------------- |
| 発生元が不明確    | どのミックスインのプロパティか追えない | ref＋分割代入で発生元が出る |
| ネームスペースの衝突 | 別作者のミックスインが同じキーを登録  | 分割代入時にリネームできる   |
| 暗黙的な相互依存   | 共有キー経由で暗黙に結合        | 戻り値を引数として渡せる    |

公式ドキュメントはこれらを理由に「Vue 3 ではミックスインを使うことをおすすめしません」と述べ、この機能はマイグレーションと馴染みのためだけに残されていると明記しています。Vue 2からの移行案件では、TypeScript化する画面や機能追加で手を入れるコンポーネントにミックスインが含まれていれば、その改修の中でコンポーザブルへ置き換えてください。触る予定のない箇所まで先回りして書き換える必要はありません。

### 設計で失敗する典型パターン

設計段階でよく見るのは、コンポーザブルを「共通処理置き場」として作ってしまう失敗です。日付整形やバリデーションのように状態を持たない処理は、素のユーティリティ関数で足ります。`use` を冠して ref を返すと、不要なリアクティブ変換と読み手の混乱を招くだけです。コンポーザブルにするのは、状態・ライフサイクルフック・ウォッチャーのいずれかを内部に持つロジックに限ってください。

呼び出しごとに独立した状態が要るのに、モジュールのトップレベルで `ref()` を宣言してしまう書き方も避けます。

```
// NG: モジュールのトップレベルなので全コンポーネントで1つの状態を共有する
import { ref } from 'vue'

const count = ref(0)
export function useCounter() {
  return { count, increment: () => count.value++ }
}
```

この形では、どの画面から呼んでも同じ `count` を触ります。コンポーネントごとに独立した状態がほしいなら、宣言を関数の内側へ移します。

```
// OK: 呼び出しごとに新しい状態を作る
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  return { count, increment: () => count.value++ }
}
```

NG側の状態はアプリケーション全体で共有されるシングルトンになり、コンポーネントごとに独立していると誤解したまま使うと別画面の値が混ざります。共有そのものが目的なら、トップレベルに置く書き方は小規模な共有状態の実装として成り立ちます。どちらの意図なのかを関数名やコメントで明示してください。ただしSSRではモジュールのトップレベルの状態がリクエスト間で共有されてしまうため、公式の状態管理ガイドはこのシングルトンに起因する問題に注意を促しています。コンポーザブルの単体テストは、[Viteネイティブなテスト基盤であるVitest](/tech/details/16023/)のようにブラウザ環境を切り替えられるランナーと組み合わせると書きやすくなります。

## TypeScriptとの型連携

### propsとemitsの型ベース宣言

公式FAQは、Options APIが2013年に生まれた当時は型推論を考慮せずに設計されており、型推論を持ち込むには「とてもありえない複雑な型定義」が必要だったと振り返っています。Composition APIは素の変数と関数なので、型はそのまま通ります。`defineProps` と `defineEmits` の宣言も型引数だけで済みます。

```
<script setup lang="ts">
interface Props {
  userId: number
  label?: string
}

const props = withDefaults(defineProps<Props>(), { label: '未設定' })

const emit = defineEmits<{
  change: [id: number]
  submit: [payload: { id: number; label: string }]
}>()

function apply() {
  emit('submit', { id: props.userId, label: props.label })
}
</script>
```

`withDefaults` は型ベース宣言に既定値を与えるコンパイラマクロです。`defineEmits` の型はタプル構文で書き、イベント名ごとに引数の型を並べます。発火側の引数の過不足は、エディタの型チェックや `vue-tsc --noEmit` の実行時に検出されます。Viteの開発サーバーやビルドは型を取り除いて変換するだけで型エラーでは止まらないため、CIで `vue-tsc` を走らせてください。

### defineModelとprops分割代入

双方向バインディングは `defineModel()` で書きます。Vue 3.4以降で使え、props と emit を手で組み合わせていた従来のパターンが1行になりました。

```
<!-- 子: SearchBox.vue -->
<script setup lang="ts">
const model = defineModel<string>({ required: true })   // 3.4 以降
</script>

<template>
  <input v-model="model">
</template>
```

親側は通常の `v-model` を書くだけです。

```
<!-- 親: SearchPanel.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import SearchBox from './SearchBox.vue'

const keyword = ref('')
</script>

<template>
  <SearchBox v-model="keyword" />
</template>
```

Vue 3.5以降では、`defineProps` の戻り値を分割代入してもリアクティビティが保たれます。`reactive()` の分割代入とは結果が逆になるので、バージョン境界に注意してください。

```
<script setup lang="ts">
import { watch } from 'vue'

// 3.5 以降は分割代入してもリアクティビティが保たれる
const { userId, label = '未設定' } = defineProps<{
  userId: number
  label?: string
}>()

// コンパイラが props.userId への参照に置き換えるため watch も効く
watch(() => userId, (v) => console.log(v))
</script>
```

この機能は、コンパイラが変数参照を `props.userId` への参照に書き換えることで成り立っています。3.4以前のプロジェクトで同じコードを書くと値が固定されるため、package.json のVueのバージョンを確かめてから使ってください。

## Options APIとの違いと選定基準

### 各オプションに対応する関数

Options APIから読み替えるときは、オプション名と関数の対応を押さえるのが近道です。ライフサイクルフックは `on` を冠した関数になります。Vue 2の `beforeDestroy` と `destroyed` は、Vue 3のOptions APIでは `beforeUnmount` と `unmounted` に名前が変わり、Composition APIではそれぞれ `onBeforeUnmount()` と `onUnmounted()` に対応します。

| Options API         | Composition API             |
| ------------------- | --------------------------- |
| data                | ref() / reactive()          |
| computed            | computed()                  |
| methods             | 通常の関数                       |
| watch               | watch() / watchEffect()     |
| mounted / unmounted | onMounted() / onUnmounted() |
| provide / inject    | provide() / inject()        |
| mixins              | コンポーザブル関数                   |

対応表のうち書き方が大きく変わるのは `provide` と `inject` です。深い階層へ値を配る用途は同じですが、Composition APIでは関数呼び出しになり、注入側は第2引数で既定値を指定できます。

```
<!-- 祖先: Layout.vue -->
<script setup>
import { provide, readonly, ref } from 'vue'

const theme = ref('dark')
provide('theme', readonly(theme))   // 子孫の書き換えを防ぐ
</script>
```

受け取る側は `inject` を呼びます。あわせて押さえておきたいのが `defineExpose` です。`<script setup>` のコンポーネントは既定で閉じており、親はテンプレート参照経由でも中身に触れません。親に公開したいプロパティやメソッドだけを明示的に列挙します。

```
<!-- 子孫: Button.vue -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light')   // 第2引数は既定値

// script setup は既定で閉じているため、親に公開する API は明示する
defineExpose({ theme })
</script>
```

提供側で `readonly()` を通しておくと、子孫が値を直接書き換える経路を塞げます。子孫から状態を更新させたいときは、更新用の関数もあわせて提供します。

### 記述構造・型推論・バンドルサイズの差

両者の実務上の差は次の4点です。

| 観点      | Options API    | Composition API |
| ------- | -------------- | --------------- |
| コードの配置  | オプション名で強制的に整理  | 関心事ごとに自由配置      |
| ロジック再利用 | ミックスイン         | コンポーザブル関数       |
| 型推論     | ミックスイン・DIで制約あり | 素の変数・関数として通る    |
| 縮小化     | インスタンスプロキシ経由   | 変数へ直接アクセス       |

Composition APIだけで書く構成なら、[コンパイル時フラグ](https://ja.vuejs.org/api/compile-time-flags)の `__VUE_OPTIONS_API__` を `false` にしてOptions API関連のコードを省き、プロダクションバンドルを数キロバイト削れると公式FAQは説明しています。ただしこのフラグは依存関係の中のVueコンポーネントにも効きます。Options APIで書かれたサードパーティ製コンポーネントを使っているなら、動作を確かめてから有効にしてください。かつて使われたクラス構文（Class API）については、公式FAQが「Vue 3 においてはもはや Class API を推奨してません」と明記しています。

### 公式推奨と実務での判断基準

公式ドキュメントのプロダクション向け推奨は、ビルドツールを使わない構成やプログレッシブエンハンスメントのように複雑性の低い場面ではOptions API、アプリケーション全体をVueで構築するならComposition APIと単一ファイルコンポーネントの組み合わせ、というものです。

2026年に新規で書き始めるVueアプリケーションは、Composition APIを既定にすべきです。理由は好みではなく2つあります。TypeScriptを前提にすると、Options APIの型推論はミックスインや依存関係の注入を使った箇所で成り立たなくなることがあること、そしてVapor ModeがOptions APIを受け付けないことです。逆に、既存のOptions APIのコードベースを動機なく一括で書き換えるのは勧めません。Options APIに廃止予定はなく、書き換えは回帰テストのコストだけが先に発生します。[Vue.jsとの違いから採用判断までを整理したNuxt](/tech/details/15790/)の上で新規開発する場合も、判断の軸は変わりません。Vueでの新規開発を外部に任せる場合は、[フルスクラッチ開発](/service/system/fullscratch/)の段階でComposition APIとTypeScriptを前提にした設計を指定しておくと、後から書き方が混在しにくくなります。

### 同一コンポーネントでの併用可否

同じコンポーネントの中で両方のAPIを使うことはできます。Options APIのコンポーネントに `setup()` オプションを足す形です。

```
<script>
import { ref } from 'vue'

export default {
  data() {
    return { legacy: true }     // 既存の Options API はそのまま
  },
  setup() {
    const count = ref(0)        // 新しい機能だけ Composition API で書く
    return { count }
  }
}
</script>
```

ただし公式FAQは、この形を採るのは「既存のOptions APIコードベースがあり、Composition APIで書かれた新しい機能や外部ライブラリーと統合する必要がある場合」に限るとしています。新規コンポーネントで両方を混ぜる理由はなく、移行期の橋渡しと割り切って使う機能です。

## React Hooksとの違い

### 実行モデルと依存配列の差

公式FAQによれば、Composition APIはReact Hooksと同じ水準のロジック構成機能を持ちながら、実行モデルが違います。React Hooksはコンポーネントが更新されるたびに関数全体が再実行されます。Composition APIの `setup()` や `<script setup>` は1回だけ実行され、その後の更新はリアクティブシステムが必要な箇所だけを再計算します。同じ「キーワードで絞り込み、件数をタイトルに出す」処理を並べると、差は依存配列の有無に表れます。

```
// React: 再レンダーのたびにこの関数全体が再実行される
import { useState, useMemo, useEffect } from 'react'

function Search({ items }) {
  const [keyword, setKeyword] = useState('')
  const filtered = useMemo(
    () => items.filter((i) => i.includes(keyword)),
    [items, keyword]          // 依存配列は手で書く
  )
  useEffect(() => {
    document.title = `${filtered.length}件`
  }, [filtered])
  return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
}
```

```
<!-- Vue: setup は1回だけ実行され、依存は自動で追跡される -->
<script setup>
import { ref, computed, watchEffect, onMounted } from 'vue'

const props = defineProps({ items: Array })
const keyword = ref('')
const filtered = computed(() => props.items.filter((i) => i.includes(keyword.value)))
// DOM への書き込みはブラウザだけで行う（useEffect がサーバーで実行されないのと揃える）
onMounted(() => {
  watchEffect(() => {
    document.title = `${filtered.value.length}件`
  })
})
</script>

<template>
  <input v-model="keyword">
</template>
```

Vue側は `keyword` と `props.items` のどちらが変わっても `filtered` と `watchEffect` が再実行されます。依存を列挙していないのに追随するのは、実行時に読まれたリアクティブな値をVueが記録しているからです。Hooksにある「条件分岐の中で呼べない」「依存配列の書き漏らしで古い値を掴む（stale closure）」という制約も、Composition APIにはありません。公式FAQは、Composition APIの呼び出しは順番に関係なく条件付きで呼べると明記しています。なお、`watchEffect` を `onMounted` の中に置いたのは、`watchEffect` が作成直後に1回実行されるためです。SSRで `setup` 直下に書くと、サーバーで `document` に触れて落ちます（vue 3.5.44で確認）。

### React Compiler 1.0の後も残る差

公式FAQは、Hooksのメモ化の問題の一部は「今後リリースされる React Compiler によって解決される可能性があります」と書いています。この前提はすでに古くなりました。[React Compiler 1.0](https://react.dev/blog/2025/10/07/react-compiler-1)は2025年10月7日に初の安定版として公開され、ビルド時にコンポーネントとフックを自動でメモ化します。`useMemo` と `useCallback` を手で書く必要は大きく減りました。

それでも、再レンダーのたびに関数を再実行するモデル自体は変わりません。同じ告知は、メモ化のされ方が変わると、その値を依存に持つ `useEffect` が多く発火したり発火しなくなったりすることがあると注意しています。`useMemo` と `useCallback` も、エフェクトの依存を安定させる用途では手で書く逃げ道として残っています。Vueでは `setup` が1回しか走らないので、この種の調整は最初から発生しません。ReactとVueのどちらで作るかの判断は[ReactとVue 3の違いを比較した記事](/tech/details/2348/)、Compiler側の導入手順は[React Compilerの導入とuseMemoの扱いを解説した記事](/tech/details/9415/)で扱っています。

## Vue 3.5・3.6世代で変わった前提

### 3.5で追加されたヘルパー

2020〜2022年に書かれたコード例を読むときは、次のAPIが無い前提になっていないかを確かめてください。Vue 3.5では `useTemplateRef()`、`useId()`、`onWatcherCleanup()` が追加されました。

```
<script setup>
import { useTemplateRef, useId, onMounted } from 'vue'

const inputRef = useTemplateRef('search')   // 3.5 以降。ref 変数名と一致させなくてよい
const fieldId = useId()                     // 3.5 以降。SSR でも安定する一意 ID

onMounted(() => inputRef.value?.focus())
</script>

<template>
  <label :for="fieldId">検索</label>
  <input :id="fieldId" ref="search">
</template>
```

`useTemplateRef()` はテンプレート参照をキー文字列で取得するため、変数名とテンプレート側の `ref` 属性名を揃える必要がなくなりました。`useId()` はサーバーとクライアントのレンダー間で同じ値になる一意のIDを生成するので、フォームのラベル紐付けやアクセシビリティ属性に使ってもハイドレーションミスマッチを起こしません。[CSR・SSR・SSGの違いと選定基準](/tech/details/13578/)を踏まえてSSRを選ぶ構成では、この保証の有無が実装の手間に直結します。

### Vapor ModeがOptions API非対応であることの意味

Vue 3.6は2026年7月18日の3.6.0-rc.1でRCに入り、10月30日の3.6.0-rc.14まで毎週のようにRCを重ねています。安定版の系列は3.5のままで、最新は10月13日の3.5.44です（版と公開日はnpmレジストリの記録による）。[v3.6.0-rc.1のリリースノート](https://github.com/vuejs/core/releases/tag/v3.6.0-rc.1)は、Vapor Modeの機能が揃ったことをRC入りの理由に挙げ、同じ3.6で `@vue/reactivity` をalien-signalsベースへ大きく作り直し、リアクティビティシステムの性能とメモリ使用量を改善したと説明しています。

Vapor ModeはSFC向けの新しいコンパイルモードで、ベースラインのバンドルサイズ削減と性能向上を狙っています。完全にオプトインで、既存のVue APIの一部をほぼ同じ挙動で使えます。リリースノートには、第三者のベンチマークでSolidやSvelte 5と同水準の性能が出たとも書かれています。Composition APIの選定に直結するのは、有効化の書き方です。

```
<!-- Vapor Mode は <script setup> 系のみ。Options API では使えない -->
<script setup vapor>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>
```

同じリリースノートは、Vapor Modeが対応するのはテンプレートのみのSFCと `<script setup>` を使うSFCで、Options APIは対象外だと明記しています。VNodeやコンポーネントのpublic instance proxyに依存する機能も、Vaporコンポーネントでは使えません。将来Vapor Modeで性能を稼ぐ余地を残したいなら、そのコンポーネントは `<script setup>` で書いておく必要があります。

とはいえ、いま全面採用を勧める段階ではありません。リリースノートが挙げる用途は、既存アプリで性能が重要なページだけをVapor Modeで実装することと、小規模な新規アプリ全体をVapor Modeで作ることの2つです。プロダクションは3.5系で組み、3.6は検証環境で試してください。Nuxt上で使う場合の版の確認方法は[Nuxt 4の最新バージョンと導入手順](/tech/details/9938/)にまとめています。

## よくある質問

### Options APIとは何ですか？

Vueのコンポーネントを `data`・`computed`・`methods`・`mounted` などのオプションを持つ1つのオブジェクトとして書くスタイルです。値や処理には `this` 経由でアクセスします。公式ドキュメントは「Options APIは Composition API を土台にしています」と説明しており、内部の仕組みは共通です。ビルドツールを使わない構成や複雑性の低い画面に向いています。

### Vue 2でもComposition APIは使えますか？

2022年7月に出たVue 2.7に組み込まれているので使えます。ただしVue 2.7はVue 2系の最後のマイナーリリースで、[Vue 2は2023年12月31日にサポートを終了](https://v2.vuejs.org/lts/)しました。セキュリティ更新が必要なら有償の延長サポートを検討するか、Vue 3への移行を計画してください。

### Composition APIはすべてのユースケースをカバーしていますか？

状態を持つロジックについてはカバーしています。公式FAQによれば、Composition APIで別途オプションが要るのは `props`、`emits`、`name`、`inheritAttrs` の4つです。このうち `name` と `inheritAttrs` は、3.3以降なら `<script setup>` の中で `defineOptions` を使って設定できます。

### Vue 3の最新バージョンはどれですか？

2026年11月2日時点の安定版は3.5.44で、2026年10月13日に公開されました。次のマイナーの3.6は10月30日公開の3.6.0-rc.14までRCが続いており、安定版は出ていません。プロダクションでは3.5系を使い、3.6はVapor Modeの検証用途に留めてください。

### Composition APIはコードが整理しにくくなりませんか？

公式FAQもこの指摘を認めています。そのうえで、Composition APIは「コードをそれぞれのバケツに入れるよう導くガードレール」を持たない代わりに、通常のJavaScriptと同じコード整理のやり方を適用できるし適用すべきだ、と述べています。Options APIは考えることを減らせる一方、決まった構成パターンに縛られ、大規模になるとリファクタリングが難しくなります。整理の責任が仕組みの側から書き手の側へ移ると考えてください。

## 関連記事

- [Piniaとは？読み方・使い方とVuexとの違い【Pinia 4.1対応】](/tech/details/3513/)
- [ReactとVue 3の違いを比較｜どっちを選ぶべきか【React 19.3・Vue 3.5対応】](/tech/details/2348/)
- [Nuxtとは：Vue.jsとの違い・レンダリング方式・4系への移行手順と採用判断を実装視点で解説](/tech/details/15790/)
- [Vitestとは？Viteネイティブなテスト基盤の仕組みと採用判断を実装目線で解説](/tech/details/16023/)
- [Next.jsとNuxtの違いを比較｜React・Vueとの関係と選び方【Next.js 16.4・Nuxt 4.6】](/tech/details/2832/)

---

出典: [Composition APIとは？Options API・React Hooksとの違いとコンポーザブルの書き方【Vue 3.5】](<https://www.issoh.co.jp/tech/details/3589/>)（株式会社一創）
