Vue 3のComposition APIは、コンポーネントのロジックを、インポートしたAPIの関数で組み立てる書き方です。関連する状態と処理を、機能ごとにまとめやすくなります。この記事では、基本のrefから、つまずきやすいreactiveの制限、<script setup>とsetup()関数の違い、Composableへの切り出しまでを整理します。
Options APIとComposition APIの選び方
Vueのコンポーネントには、Options APIとComposition APIの2つの書き方があります。公式は、どちらも一般的なユースケースを十分に扱え、同じ仕組みの上に作られていると説明しています(Options APIは、Composition APIの上に実装されています)。どちらかが古い、劣っているということではありません。
- Options API:
data、methods、mountedのようなオプションで、ロジックを定義します。ビルドツールを使わない場合や、複雑でない場面(既存のページへの部分的な導入など)に向いています。 - Composition API:インポートした関数でロジックを組み立てます。本格的なアプリを、単一ファイルコンポーネント(SFC)で作る場合に向いています。
学習のうちは、読みやすいと感じるほうで構いません。核になる概念は共通で、あとからもう一方を覚えられます。この記事では、Composition APIを説明します。
基本:refでリアクティブな変数を作る
SFCでは、Composition APIを<script setup>と一緒に使うのが一般的です。ref()に初期値を渡すと、リアクティブな変数ができます。スクリプトの中では.valueで読み書きし、テンプレートの中では.valueが不要です。
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++ // スクリプトの中では .value が必要
}
</script>
<template>
<!-- テンプレートの中では、.value は不要(自動で取り出される) -->
<p>カウント: {{ count }}</p>
<button type="button" @click="increment">増やす</button>
</template>
<script setup>とsetup()関数の違い
Composition APIには、書き方が2つあります。<script setup>は、コンパイル時の変換を使って、トップレベルの変数や関数を自動でテンプレートに公開します。ボイラープレートが少なくなるため、SFCでは、こちらが一般的です。もう一方のsetup()関数は、オプションとして書き、テンプレートで使うものを自分でreturnする必要があります。
<script>
import { ref } from 'vue'
// setup() 関数を使う書き方:テンプレートで使うものは、return で公開する
export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment } // return し忘れると、テンプレートから見えない
}
}
</script>
テンプレートで .value が不要になるのは、トップレベルだけ
テンプレートでrefが自動で取り出されるのは、トップレベルのプロパティだけです。オブジェクトの中にネストしたrefは、取り出されません。
<script setup>
import { ref } from 'vue'
const count = ref(0)
const object = { id: ref(1) }
const { id } = object // トップレベルの変数に取り出す
</script>
<template>
{{ count + 1 }} <!-- OK:トップレベルの ref は展開される → 1 -->
{{ object.id + 1 }} <!-- NG:ネストした ref は展開されない → "[object Object]1" -->
{{ id + 1 }} <!-- OK:取り出した変数なら展開される → 2 -->
{{ object.id }} <!-- 最終的な値としての出力は展開される → 1 -->
</template>
複数の状態をまとめる:reactiveと、その制限
関連する複数の値を、1つのオブジェクトにまとめたいときは、reactiveが使えます。
<script setup>
import { reactive } from 'vue'
const state = reactive({ name: '', age: 0 })
function reset() {
state.name = ''
state.age = 0
}
</script>
ただし、reactiveには、公式が明記している制限が3つあります。
import { reactive } from 'vue'
// 制限① オブジェクト型にしか使えない(文字列・数値・真偽値は持てない)
// 制限② 丸ごと置き換えると、リアクティブな接続が失われる
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // 最初のオブジェクトは、もう追跡されない
// 制限③ 分割代入や、関数への受け渡しで、接続が切れる
const s = reactive({ count: 0 })
let { count } = s
count++ // s には反映されない
callSomeFunction(s.count) // ただの数値が渡るので、変化を追えない
- オブジェクト型にしか使えない:オブジェクト、配列、
MapやSetなどに限られ、文字列・数値・真偽値は持てません。 - 丸ごと置き換えられない:Vueの追跡は、プロパティへのアクセスで行われるため、同じ参照を保つ必要があります。
- 分割代入に弱い:プリミティブのプロパティを、ローカル変数に取り出したり、関数に渡したりすると、接続が切れます。
こうした制限があるため、公式は、リアクティブな状態の宣言には、ref()を主なAPIとして使うことを推奨しています。複数の値でも、まずはrefを1つずつ使うのが、迷わない方法です。
reactiveを分割代入したいときは toRefs
どうしてもreactiveを使い、分割代入もしたい場合は、toRefs()でrefのまとまりに変えます。作られたrefは、元のオブジェクトと同期します。単一のプロパティだけなら、toRef(state, 'foo')です。
import { reactive, toRefs } from 'vue'
function useFeature() {
const state = reactive({ foo: 1, bar: 2 })
// ... state を使った処理 ...
// 返すときに toRefs で ref のまとまりにすると、分割代入しても接続が切れない
return toRefs(state)
}
const { foo, bar } = useFeature()
foo.value++ // もとの state.foo も更新される
ライフサイクルフック
Options APIのmountedなどに対応する関数(onMountedなど)を、インポートして使います。setupの実行中に、同期的に呼ぶ必要があります。
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
// ライフサイクルフックは、関数としてインポートして使う。
// setup の実行中に、同期的に呼んで登録する
onMounted(() => {
console.log('マウントされました')
})
onBeforeUnmount(() => {
console.log('破棄される直前です')
})
</script>
算出プロパティ・ウォッチャー・props
Composition APIでは、これらも関数として書きます。それぞれ、詳しい解説の記事があります。
- 算出プロパティはcomputedプロパティで動的なデータ処理を行う方法
- 変化に反応する処理はwatchを活用してデータの変化を監視する方法、使い分けはwatch・watchEffectの違いと使い分け
- 親子のやり取り(
defineProps/defineEmits)はコンポーネント間でデータをやりとりする方法
Composableとして切り出す
Composition APIの強みは、状態とロジックを、関数にまとめて再利用できることです。こうした関数を「Composable」と呼び、名前はuseで始めます。refを返せば、受け取る側で分割代入しても、リアクティブのままです。
// useCounter.js:状態とロジックを、関数にまとめる
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment } // ref を返す(分割代入しても、リアクティブのまま)
}
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>
より実用的な作り方はComposable関数でロジックを共通化する方法をご覧ください。
よくある質問(FAQ)
ref()を主なAPIとして使うことを推奨しています。reactiveは、オブジェクト型にしか使えず、丸ごと置き換えたり、分割代入したりすると、リアクティブな接続が切れるためです。toRefs()でrefのまとまりに変えてから分割代入するか、最初からrefを使ってください。<script setup>は、トップレベルの変数や関数を、自動でテンプレートに公開する書き方で、SFCでは一般的です。setup()関数は、テンプレートで使うものをreturnする必要があります。refだけです。ネストしたrefは、const { id } = objectのように、トップレベルの変数に取り出してください。まとめ
Composition APIは、<script setup>とrefを軸に、状態とロジックを機能ごとにまとめる書き方です。reactiveの3つの制限に注意し、迷ったらrefを使いましょう。共通のロジックは、Composableに切り出して、再利用してください。

