リアクティビティーの基礎
API の参照
このページと、当ガイドの多くの章では、Options API と Composition API で異なる内容が含まれています。現在の設定は Composition API です。左サイドバーの上部にある「API の参照」スイッチで、API スタイルを切り替えることができます。
リアクティブな状態を宣言する
ref()
Composition API では、リアクティブな状態を宣言する方法として、ref()
関数を使用することを推奨します:
js
import { ref } from 'vue'
const count = ref(0)
ref()
は、引数を受け取り、それを .value
プロパティを持つ ref オブジェクトにラップして返します:
js
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
参照: ref の型付け
コンポーネントのテンプレート内で ref にアクセスするには、下記に示すように、コンポーネントの setup()
関数で宣言し、それを返します:
js
import { ref } from 'vue'
export default {
// `setup` は、Composition API 専用の特別なフックです。
setup() {
const count = ref(0)
// ref をテンプレートに公開します
return {
count
}
}
}
template
<div>{{ count }}</div>
テンプレート内で ref を使用する際、.value
をつける必要はないことに注意してください。便利なように、ref はテンプレート内で使用されると自動的にアンラップされます(いくつかの注意点があります)。
イベントハンドラーで直接 ref を変更することもできます:
template
<button @click="count++">
{{ count }}
</button>
より複雑なロジックの場合、同じスコープで ref を変更する関数を宣言し、状態とともにメソッドとして公開できます:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// JavaScript 内では .value が必要
count.value++
}
// 関数も公開することを忘れないでください。
return {
count,
increment
}
}
}
公開されたメソッドは、イベントハンドラーとして使用できます:
template
<button @click="increment">
{{ count }}
</button>
これは、ビルドツールを使わずに、Codepen 上で実行されている例です。
<script setup>
setup()
で状態やメソッドを手動で公開するのは冗長になりがちです。幸い、単一ファイルコンポーネント(SFC) を使用すれば、これを避けられます。<script setup>
によって使い方を簡略化できます:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
<script setup>
で宣言されたトップレベルのインポート、変数、関数は、同じコンポーネントのテンプレートで自動的に使用可能になります。テンプレートは同じスコープで宣言された JavaScript の関数と同じだと考えれば、当然ながら、一緒に宣言されたすべてのものにアクセスできます。
TIP
このガイドの残りの部分では、Composition API のコード例には主に SFC + <script setup>
構文を使用します。これは、Vue 開発者にとって最も一般的な使用方法だからです。
SFC を使用しない場合でも、setup()
オプションで Composition API を使用できます。
ref を使う理由
なぜ普通の変数ではなく、.value
を使った ref が必要なのか、疑問に思うかもしれません。それを説明するために、Vue のリアクティビティシステムの仕組みについて簡単に説明する必要があります。
テンプレート内で ref を使用し、後から ref の値を変更した場合、Vue は自動的にその変更を検出し、それに応じて DOM を更新します。これは、依存関係追跡ベースのリアクティビティーシステムによって実現されています。コンポーネントが初めてレンダリングされるとき、Vue はレンダリング中に使用されたすべての ref を追跡します。その後 ref が変更されると、それを追跡しているコンポーネントの再レンダリングがトリガーされます。
標準的な JavaScript では、普通の変数のアクセスや変更を検出する方法はありません。しかし getter/setter メソッドを使えば、オブジェクトのプロパティの get や set の操作をインターセプトできます。
.value
プロパティは、Vue に、ref がアクセスされたり変更されたタイミングを検出する機会を提供します。Vue は、getter でトラッキングを行い、setter でトリガーを実行する仕組みになっています。概念的には、ref は次のようなオブジェクトと考えることができます:
js
// 実際の実装ではなく、疑似コード
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
ref のもう 1 つの優れた特徴は、普通の変数と違って、最新の値やリアクティビティー接続へのアクセスを維持したまま ref を関数に渡すことができることです。これは、複雑なロジックを再利用可能なコードにリファクタリングする際、特に便利です。
リアクティビティーシステムについては、リアクティビティーの探求セクションで詳しく解説しています。
ディープなリアクティビティー
ref は、深くネストしたオブジェクトや配列、Map
のような JavaScript ビルトインのデータ構造など、どんな値の型も保持できます。
ref は、その値を深いリアクティブにします。つまり、ネストしたオブジェクトや配列を変更した場合でも、変更が検出されることが期待できます:
js
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// これらは期待通りに動作します。
obj.value.nested.count++
obj.value.arr.push('baz')
}
非プリミティブ値は、後述する reactive()
を介してリアクティブプロキシーに変換されます。
また、浅い ref により、深いリアクティビティーをオプトアウトすることもできます。浅い ref では、.value
アクセスのみがリアクティビティーに追跡されます。浅い ref は、大きなオブジェクトの監視コストを回避してパフォーマンスを最適化する場合や、内部の状態を外部ライブラリーで管理する場合などに利用できます。
さらに読む:
DOM 更新のタイミング
リアクティブな状態を変化させると、DOM は自動的に更新されます。しかし、DOM の更新は同期的に適用されないことに注意する必要があります。その代わりに Vue は、更新サイクルの「next tick」まで更新をバッファリングし、どれだけ状態を変化させても、各コンポーネントは一度だけ更新することを保証しています。
状態変化後の DOM 更新が完了するのを待つため、nextTick() というグローバル API を使用できます:
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// DOM が更新されました
}
reactive()
リアクティブな状態を宣言する方法として、reactive()
という API を使う方法もあります。内側の値を特別なオブジェクトでラップする ref とは異なり、reactive()
はオブジェクト自体をリアクティブにします:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
参照: reactive の型付け
テンプレートでの使用法:
template
<button @click="state.count++">
{{ state.count }}
</button>
リアクティブオブジェクトは JavaScript プロキシ であり、通常のオブジェクトと同じように動作します。違いは、Vue がリアクティブオブジェクトのすべてのプロパティのアクセスや変更をインターセプトして、リアクティビティーの追跡やトリガーを行うことができることです。
reactive()
はオブジェクトを深く変換します。ネストしたオブジェクトもアクセスした際に reactive()
でラップされます。また、ref の値がオブジェクトである場合、内部では ref()
からも呼び出されます。浅い ref と同様に、深いリアクティビティーをオプトアウトするための shallowReactive()
API もあります。
リアクティブプロキシ vs. 独自
注意すべきは、reactive()
の戻り値が、元のオブジェクトのプロキシであり、元のオブジェクトと等しくないということです:
js
const raw = {}
const proxy = reactive(raw)
// プロキシはオリジナルと同じではありません。
console.log(proxy === raw) // false
プロキシだけがリアクティブとなります。元のオブジェクトを変更しても更新は行われません。したがって、Vue のリアクティブシステムを使用する際のベストプラクティスは、プロキシされた状態のバージョンだけを使用することになります。
プロキシへの一貫したアクセスを保証するために、同じオブジェクトに対して reactive()
を呼ぶと常に同じプロキシを返し、既存のプロキシに対して reactive()
を呼ぶとその同じプロキシも返されます。
js
// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true
// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true
このルールは、ネストされたオブジェクトにも適用されます。深いリアクティビティーを持つため、リアクティブなオブジェクトの中にあるネストされたオブジェクトもプロキシとなります。
js
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive()
の制限
reactive()
API にはいくつかの制限があります:
限定された値の型: オブジェクト型 (オブジェクト、配列、および
Map
やSet
などの コレクション型) に対してのみ機能します。文字列、数値、真偽値などの プリミティブ型 を保持できません。オブジェクト全体を置換できない: Vue のリアクティビティー追跡はプロパティアクセス上で動作するため、リアクティブなオブジェクトへの参照を常に同じに保つ必要があります。つまり、最初の参照へのリアクティブな接続が失われるため、リアクティブなオブジェクトを簡単に「置き換える」ことはできません:
jslet state = reactive({ count: 0 }) // 上記の参照({ count: 0 })は、もはや追跡されていません // (リアクティブな接続は失われました!) state = reactive({ count: 1 })
分割代入できない: また、リアクティブなオブジェクトのプリミティブ型のプロパティをローカル変数に分割代入したり、そのプロパティを関数に渡したりすると、下記に示すようにリアクティブなつながりが失われることとなります:
jsconst state = reactive({ count: 0 }) // count は分割代入すると state.count と切り離されます。 let { count } = state // 元の状態に戻りません。 count++ // この関数は数値を受け取りますが、これだと // state.count の変更を追跡することができません。 // リアクティビティーを維持するためには、オブジェクト全体を渡す必要があります callSomeFunction(state.count)
このような制約があるため、リアクティブな状態を宣言するための主要な API として ref()
を使用することを推奨します。
追加の ref アンラップの詳細
リアクティブなオブジェクトのプロパティとして
ref は、リアクティブなオブジェクトのプロパティとしてアクセスまたは変更されると、自動的にアンラップされます。つまり、通常のプロパティと同じように動作します:
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
既存の ref にリンクされたプロパティに新しい ref が割り当てられると、古い ref を置き換えることになります:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 元の ref は state.count から切り離されました
console.log(count.value) // 1
ref のアンラップは、深いリアクティブオブジェクトの内部にネストされたときのみ発生します。浅いリアクティブオブジェクトのプロパティとしてアクセスされる場合には適用されません。
配列やコレクションにおける注意点
リアクティブオブジェクトとは異なり、ref がリアクティブな配列や Map
のようなネイティブコレクション型の要素としてアクセスされた場合、アンラップは行われません:
js
const books = reactive([ref('Vue 3 Guide')])
// ここでは .value が必要
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// ここでは .value が必要
console.log(map.get('count').value)
テンプレートでアンラップするときの注意点
テンプレートでの ref のアンラップは、ref がテンプレートのレンダリングコンテキストでトップレベルのプロパティである場合にのみ適用されます。
以下の例では、count
と object
はトップレベルのプロパティですが、object.id
はトップレベルではありません:
js
const count = ref(0)
const object = { id: ref(1) }
したがって、この表現は期待通りに動作します:
template
{{ count </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }}
……一方、こちらは動作しません:
template
{{ object.id </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }}
レンダリング結果は [object Object]1
となります。これは、式の評価時に object.id
がアンラップされず、ref オブジェクトのままだからです。これを修正するには、id
をトップレベルのプロパティに分割代入すればいいのです:
js
const { id } = object
template
{{ id </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }}
これで、レンダリング結果は「2」になります。
もう 1 つ注意すべきは、ref がテキスト補間(つまり{{ }}
タグ)の最終評価値である場合、アンラップされるので、以下のようにすると 1
が表示されます:
template
{{ object.id }}
これはテキスト補間の便利な機能に過ぎず、{{ object.id.value }}
と同等です。