Composition APIとは?書き方とOptions APIとの違い・移行判断を解説【2026年8月版】
Composition APIは、Vue 3でコンポーネントの状態とロジックを関数として組み立てる記述スタイルです。data や methods といったオプションの箱に振り分けるOptions APIに対して、Composition APIは ref() や computed() を素のJavaScriptの変数・関数として書き、必要な単位で切り出して再利用します。この記事では、Vue 3.5.40(2026年7月16日公開の安定版)を前提に、基本の書き方からコンポーザブル関数、TypeScript連携、Options APIとの選定基準までを実コードで整理します。
まとめ
Composition APIは <script setup> の中でリアクティブな変数と関数を宣言し、それをそのままテンプレートから使う書き方です。Options APIと機能面での優劣があるわけではなく、公式ドキュメントも「Options APIは Composition API を土台にしています」と説明しています。差が出るのは、ロジックを部品として切り出すときと、TypeScriptで型を通すときです。
選定の目安は公式の推奨がそのまま使えます。アプリケーション全体をVueで構築するならComposition API、ビルドツールを使わない構成や複雑性の低い画面ならOptions APIです。Options APIが廃止される予定はありません。公式FAQの回答は「いいえ、私たちは特にそうする予定はありません」の一文です。
ただし2026年時点で判断材料が1つ増えました。Vue 3.6でRC段階に入ったVapor Modeは、<script setup> を使うSFCとテンプレートのみのSFCだけに対応し、Options APIをサポートしません。性能改善の新しい選択肢を将来使う可能性があるなら、新規コンポーネントはComposition APIで書いておくほうが手戻りは小さくなります。以降は、書き方の具体とつまずきやすい箇所を順に見ていきましょう。
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>
注目すべきは this が出てこない点です。Options APIはコンポーネントインスタンスの this を経由して値にアクセスしますが、<script setup> のテンプレートは同じスコープ内のインライン関数としてコンパイルされ、インスタンスプロキシを挟まずに変数へ直接アクセスします。公式ドキュメントは、これによって変数名を安全に短縮でき、より良い縮小化につながると説明しています。
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() に寄せるのが実務上は安全です。ref() はプリミティブも扱え、オブジェクトを入れた場合も内部で深いリアクティブに変換されるため、reactive() でできることはほぼ包含します。@vue/reactivity 3.5.40 では ref({ a: 1 }) の a を書き換えた場合も副作用が再実行されます。
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 以降
const res = await fetch(`/api/search?q=${value}`, { signal: controller.signal })
results.value = await res.json()
})
Vue 3.5以降は onWatcherCleanup() がコアAPIとして追加され、監視対象が次に変わったときやコンポーネントの破棄時に前回の副作用を止められます。上の例では進行中のリクエストを AbortController で中断しています。同じ3.5では watch の deep に数値を渡して監視の深さを指定できるようになり、ウォッチャーの一時停止と再開もサポートされました。
リアクティビティが失われる書き方
最も踏みやすい落とし穴が reactive() の分割代入です。分割代入した瞬間にリアクティブな参照が切れ、以後の変更が追えなくなります。
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/reactivity 3.5.40 でこのコードを走らせると、1つ目の console.log は 0 1 を出力します。分割代入した count は初期値のまま取り残され、元のオブジェクト側だけが増えている状態です。toRefs() を通した2つ目は 1 で、参照が保たれています。reactive() を関数の戻り値やpropsとして受け渡すコードでは、この事故が静かに混入します。状態を関数の外へ出す場面では ref() か toRefs() を使ってください。
コンポーザブル関数によるロジックの再利用
コンポーザブルの実装と命名規則
コンポーザブル(composable)は、Composition APIを使って状態を持つロジックを閉じ込めた関数です。命名は use で始まるキャメルケースとし、リアクティブな値を返す形にします。公式ドキュメントがComposition APIの最大の利点に挙げるのが、このコンポーザブルによるロジック再利用です。
// 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 () => {
const target = toValue(url) // ref でも getter でも素の値でも受け取れる
if (!target) return
loading.value = true
try {
data.value = await (await fetch(target)).json()
error.value = null
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
return { data, error, loading }
}
引数を toValue() で受けると、呼び出し側は素の値・ref・getter関数のいずれでも渡せます。getterで渡した場合は依存が追跡され、引数の変化に応じて中の watchEffect が再実行されます。
<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を個別に返す」形が推奨されるのは、利用側でプロパティの発生元が明確になるためです。状態をアプリケーション全体で共有する段階になったら、コンポーザブルではなくVue 3対応の状態管理ライブラリであるPiniaに寄せる判断もあります。
ミックスインの3つの欠点
Vue 2でロジックを共有する手段はミックスインでした。公式ドキュメントはミックスインに3つの主要な欠点があるとし、それぞれをコンポーザブルがどう解決するかを示しています。欠点は順に、どのミックスインが注入したプロパティか追えないこと、別々の作者のミックスインが同じキーを登録して衝突すること、相互作用するミックスインが共有キー経由で暗黙に結合することです。
| ミックスインの欠点 | コンポーザブルでの解決 |
|---|---|
| 発生元が不明確 | ref+分割代入で発生元が出る |
| ネームスペースの衝突 | 分割代入時にリネームできる |
| 暗黙的な相互依存 | 戻り値を引数として渡せる |
公式ドキュメントはこれらを理由に「Vue 3 ではミックスインを使うことをおすすめしません」と述べ、この機能はマイグレーションと馴染みのためだけに残されていると明記しています。Vue 2からの移行案件でミックスインが残っている場合、まず置き換えるべき箇所はここです。
設計で失敗する典型パターン
コンポーザブルで失敗する形はほぼ1つに集約されます。「共通処理置き場」として作ってしまうことです。日付整形やバリデーションのような状態を持たない処理は、コンポーザブルではなく素のユーティリティ関数で十分であり、use を冠して ref を返すと不要なリアクティブ変換のコストと読み手の混乱を招きます。コンポーザブルにすべきなのは、状態・ライフサイクルフック・ウォッチャーのいずれかを内部に持つロジックだけです。
もう1つ、モジュールのトップレベルで 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側の状態はアプリケーション全体で共有されるシングルトンになり、コンポーネントごとに独立していると誤解したまま使うと別画面の値が混ざります。共有が目的ならPiniaのような状態管理を明示的に使い、コンポーザブルは呼び出しごとに新しい状態を作る前提を崩さないことです。コンポーザブルの単体テストは、Viteネイティブなテスト基盤であるVitestのようにブラウザ環境を切り替えられるランナーと組み合わせると扱いやすくなります。
TypeScriptとの型連携
propsとemitsの型ベース宣言
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 の型はタプル構文で書き、イベント名ごとに引数の型を並べます。この書き方なら、発火側の引数の過不足はコンパイル時に検出されるようになります。
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 を冠した関数になり、beforeDestroy と destroyed はVue 3で 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だけを使う構成ならコンパイル時フラグでOptions API関連のコードを省き、プロダクションバンドルを数キロバイト削減できると公式ドキュメントが説明しています。ただし公式ドキュメントは、このフラグが依存関係内のVueコンポーネントにも影響する点に注意するよう促しています。Options APIで書かれたサードパーティ製コンポーネントを使っている場合は、影響範囲を確認してから有効化してください。
公式推奨と実務での判断基準
公式ドキュメントのプロダクション向け推奨は明快です。ビルドツールを利用しない場合やプログレッシブエンハンスメントなど複雑性の低いシナリオではOptions API、アプリケーション全体をVueで構築する場合はComposition APIと単一ファイルコンポーネントの組み合わせを選びます。
ここは玉虫色にせず言い切ります。2026年に新規で書き始めるVueアプリケーションは、Composition APIを既定にすべきです。理由は好みではなく、TypeScriptを前提にしたときにOptions APIの型推論がミックスインと依存関係の注入で壊れること、そしてVapor ModeがOptions APIを受け付けないことの2点です。逆に、既存のOptions APIコードベースを動機なく一括で書き換えるのは推奨しません。Options APIに廃止予定はなく、書き換えは回帰テストのコストだけが先に発生します。Vue.jsとの違いから採用判断までを整理したNuxtのようなフレームワーク上で新規開発する場合も、判断の軸は同じです。
同一コンポーネントでの併用可否
同じコンポーネント内で両方の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>
ただし公式ドキュメントは、この形を採るのは「既存のOptions APIコードベースがあり、Composition APIで書かれた新しい機能や外部ライブラリーと統合する必要がある場合にのみ」と限定しています。新規コンポーネントで両方を混ぜる理由はありません。移行期の橋渡しと割り切って使う機能です。
Vue 3.5・3.6世代で変わった前提
3.5で追加されたヘルパー
Composition APIの解説記事の多くはVue 3.0から3.2の時代に書かれており、その後に増えた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を生成し、フォームのラベル紐付けやアクセシビリティ属性で使えます。SSRでもハイドレーションミスマッチを起こしません。CSR・SSR・SSGの違いと選定基準を踏まえた構成では、この保証の有無が実装の手間に直結します。
Vapor ModeがOptions API非対応であることの意味
2026年7月18日にVue 3.6がRCフェーズに入りました。リリースノートは、Vapor Modeの意図した機能セットが完成したことがRC移行の理由だと述べています。同じ3.6では @vue/reactivity がalien-signalsをベースに大幅にリファクタされ、リアクティビティシステムの性能とメモリ使用量が改善されています。
Vapor ModeはSFC向けの新しいコンパイルモードで、狙いはベースラインのバンドルサイズ削減と性能向上です。100パーセントオプトインで、既存の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はテンプレートのみの単一ファイルコンポーネントと <script setup> を使う単一ファイルコンポーネントをサポートし、Options APIはサポートされない」と明記しています。VNodeやコンポーネントのpublic instance proxyに依存する機能もVaporコンポーネントでは使えません。つまり、将来Vapor Modeで性能を稼ぐ選択肢を残したいなら、そのコンポーネントは <script setup> で書かれている必要があります。
とはいえ現時点で全面採用を勧める段階ではありません。リリースノートが挙げる推奨用途は、既存アプリで性能が重要なページを部分的にVapor Modeで実装すること、および小規模な新規アプリ全体をVapor Modeで構築することの2つです。3.6はまだRCであり、安定版は3.5.40です。フレームワークの選定段階から比較しているなら、React系とVue系の対応関係を整理したNext.jsとNuxtの比較も判断材料になります。
よくある質問
VueのAPIには何種類ありますか?
Options APIとComposition APIの2種類です。インターフェースは異なりますが、それを支える基盤のシステムは同一で、公式ドキュメントは「Options APIはComposition APIを土台にしています」と説明しています。どちらのスタイルでもよくあるユースケースは全面的にカバーされます。
Vue 2でもComposition APIは使えますか?
2022年7月に出荷されたVue 2.7に組み込まれており、使えます。ただしVue 2.7はVue 2系の最後のマイナーリリースで、Vue 2は2023年12月31日にEnd of Lifeに到達しました。セキュリティ更新が必要な場合はVue 2 Extended LTSを検討するか、Vue 3への移行を計画してください。
Composition APIはすべてのユースケースをカバーしていますか?
状態を持つロジックについてはカバーしています。公式ドキュメントによれば、Composition APIを使うときに別途オプションが必要になるのは props、emits、name、inheritAttrs の4つです。このうち name と inheritAttrs は、3.3以降であれば <script setup> 内の defineOptions で直接設定できます。
Vue 3の最新バージョンはどれですか?
2026年8月5日時点の安定版は3.5.40で、2026年7月16日に公開されました。次のマイナーである3.6は2026年7月22日公開の3.6.0-rc.2までRC段階が進んでおり、まだ安定版ではありません。プロダクションでは3.5系を使い、3.6はVapor Modeの検証用途に留めるのが無難です。
Composition APIはコードが整理しにくくなりませんか?
公式ドキュメントもこの指摘を認めたうえで、Composition APIは「コードをそれぞれのバケツに入れるよう導くガードレール」を提供しなくなった代わりに、通常のJavaScriptと同じコード整理のベストプラクティスを適用できるし適用すべきだ、と述べています。Options APIは考えることを減らせる一方、規定の構成パターンに縛られ、大規模プロジェクトではリファクタリングが難しくなる可能性があります。整理の責任が言語側から書き手側へ移る、という理解が実態に近いでしょう。