Vue 3 のリアクティブデータ:ref と reactive の完全ガイド
Vue 3のリアクティブシステムは、その心臓部です。変数を変更すると、ページ上でその変数が使用されているすべての箇所が自動的に更新されます。だからこそ、VueはjQueryに比べて10倍も簡単にコーディングできるのです。このリアクティブシステムは、ref()とreactive()という2つのコアAPIによって支えられています。
ref は あらゆる種類のデータ(数値、文字列、オブジェクト)をラップするために使用されますが、reactive は オブジェクトのみをラップできます。この 2 つは使い方が異なり、このレッスンではその違いを完全に理解できるようになります。
1. 学習内容
- リアクティブデータの本質:プロキシによるインターセプト + 依存関係の収集 + 更新のトリガー
ref()任意の値をラップするための構文、.valueそれにアクセスするための構文reactive()オブジェクトのパッケージ化、深度への自動応答- 「Ref」と「Reactive」の5つの主な違い、およびそれぞれの使用場面
- shallowRef / shallowReactive 浅いリアクティブ性
- toRef / toRefs ユーティリティ関数の使い方
- レスポンシブデザインを失敗に導く5つのよくある落とし穴
2. うまくいかなかったカウンターページの物語
(1) 課題:カウントが変更されても、UIが更新されない
アリスは、ECサイトの管理画面向けに、初めてのVue 3コンポーネントを作成しました。彼女は「商品の在庫カウンター」を実装しようと試みました:
// ❌ The "Flip" Version
let count = 0
function increment() {
count++
console.log(count) // 1, 2, 3... It looks like
}
<template>
<p>Stock: {{ count }}</p>
<button @click="increment">+1</button>
</template>
彼女はボタンを5回クリックした。コンソールには1、2、3、4、5と表示された。しかし、ページには依然として「在庫:0」と表示されたままである。
プロダクトマネージャーのチャーリーが、彼女の肩越しに覗き込む:
「アリス、カウントが更新されないんだけど? 5回クリックしたのに。Vueが壊れてるのかな?」
(2) Vueのリアクティブソリューション:ref() / reactive()
<!-- ✅ Correct Version -->
<template>
<p>Stock: {{ count }}</p>
<button @click="increment">+1</button>
</template>
<script setup>
import { ref } from 'vue'
// Use ref() Wrapper → Vue Knows it should be \"Reactive\"
const count = ref(0)
function increment() {
// In JS Key Points .value
count.value++
// Automatic unpacking in templates,Not necessary .value
}
</script>
このボタンをクリックすると、ページが即座に更新されます。変数が変更されると、DOMが自動的に更新されます。
(3) 収益
Vue 3 のリアクティブ性を学んだ後:
- コード量:jQueryによるDOM操作 30行 → Vue 5行
- バグ発生率:50%(DOMの更新を忘れる)→ 0%(Vueが処理してくれる)
- 認知負荷:「この変数に依存しているDOM要素はどれか?」→「すべて、自動的に」
3. リアクティビティの原則:プロキシと依存関係の収集
(1) 3段階の対応プロセス
sequenceDiagram
participant T as Template
participant C as Component
participant R as Reactive<br/>(Proxy)
participant D as Data
T->>C: 1. Render (Read count)
C->>R: 2. get count
R->>D: 3. Read 0
R->>R: 4. Track: Who's using it? count?
R-->>C: 5. Return 0
C-->>T: 6. Display "Stock: 0"
Note over D,T: The user clicks the button
D->>R: 7. set count = 1
R->>R: 8. Trigger: Notice to All Users count the place
R->>C: 9. Re-render
C-->>T: 10. Display "Stock: 1"
(2) 要点
- プロキシによるインターセプト:Vue 3 では、ES6 のプロキシを使用して get/set 操作をインターセプトします
- 依存関係コレクション:テンプレートがレンダリングされた際に、「誰がこのデータを使用したか」(トラック)を記録します
- 更新をトリガーする:「関連するすべての場所」に対し、データが変更された際に再レンダリングを行うよう通知する(トリガー)
(3) Vue 2 対 Vue 3:リアクティブ性の比較
| 寸法 | 図面 2 | 図面 3 |
|---|---|---|
| 基盤となるAPI | Object.defineProperty |
ES6 Proxy |
| 配列ウォッチャー | 特別な処理が必要 | ネイティブサポート |
| マップ/セットのウォッチ | 未対応 | 対応 |
| 新規物件を追加 | Vue.set() が必要 |
自動返信 |
| パフォーマンス | 平均 | 向上(レイジープロキシ) |
4. ref(): 任意の値をラップする
(1) 基本的な使い方
import { ref } from 'vue'
// Packaging Numbers
const count = ref(0)
// Packaging String
const name = ref('Alice')
// Boolean Packaging
const isVip = ref(false)
// Packaging null
const data = ref(null)
// Packaging Array
const items = ref([1, 2, 3])
// Items to Be Packaged
const user = ref({ name: 'Bob', age: 25 })
(2) 値へのアクセス:JavaScriptでは .value を使用してください。テンプレートが自動的に展開します。
<template>
<!-- Using variable names directly in templates(Automatic Unpacking) -->
<p>{{ count }}</p> <!-- 0 -->
<p>{{ name }}</p> <!-- Alice -->
<p>{{ user.name }}</p> <!-- Bob -->
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const name = ref('Alice')
const user = ref({ name: 'Bob', age: 25 })
function logCount() {
// In JS Must use .value
console.log(count.value) // 0
console.log(name.value) // Alice
console.log(user.value.name) // Bob
}
</script>
(3) refの「双方向性」
import { ref } from 'vue'
const count = ref(0)
// Read
console.log(count.value) // 0
// Edit
count.value = 1
console.log(count.value) // 1
// Modify Nested Properties(ref When wrapping an object)
const user = ref({ name: 'Bob' })
user.value.name = 'Alice' // ✅ Responsive
user.value = { name: 'Charlie' } // ✅ The entire replacement also responds
5. reactive(): ラッパーオブジェクト(ディープリアクティビティ)
(1) 基本的な使い方
import { reactive } from 'vue'
// Only objects can be passed
const state = reactive({
count: 0,
user: { name: 'Alice', age: 25 },
items: [1, 2, 3]
})
// No access required .value
console.log(state.count) // 0
console.log(state.user.name) // Alice
// Edit Auto-Reply
state.count = 1
state.user.name = 'Bob'
state.items.push(4)
(2) 詳細な回答
const state = reactive({
level1: {
level2: {
level3: {
value: 'deep'
}
}
}
})
// Changes made at any level are automatically reflected
state.level1.level2.level3.value = 'updated' // ✅ Trigger an update
(3) リアクティブプログラミングの限界
// ❌ You cannot directly replace the entire object.(No longer responsive)
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // Wrong! state variable itself changed, but template uses old reference.
// ❌ Deconstruction results in the loss of responsiveness
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = state // count/name Convert to a regular variable,Not responding
// ✅ Destructuring Reactivity: use toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // count/name.value Stay Responsive
6. ref と reactive の5つの主な違い
| 側面 | ref | 反応性 |
|---|---|---|
| ラップ可能な型 | 任意の値(数値、文字列、オブジェクト、配列) | オブジェクトのみ(Object/Array) |
| アクセス方法 | .value (JS) |
直接アクセス (JS) |
| テンプレートのアクセス | 自動展開(.value なし) | 直接アクセス |
| すべて置換 | ref.value = {...} ✅ 反応あり |
reactive({...}) = ... ❌ 反応なし |
| 分解 | 応答は保持される (toRefs) | 応答は失われる (toRefs が必要) |
(4) 選定に関する推奨事項
| シナリオ | 推奨事項 |
|---|---|
| プリミティブ型(数値/文字列/ブール値) | ref() |
| 単一のレスポンシブオブジェクト | ref({}) または reactive({}) のいずれか |
| 複数の関連するフィールドで構成される状態 | reactive({})(Vuexのようなもの) |
| 完全に交換が必要 | ref({}) (より柔軟性が高い) |
| プロップの受け渡し/発光 | ref() (境界を明確にする) |
7. 浅いリアクティブ性:shallowRef / shallowReactive
(1) 表面的な回答はどのような場合に用いるべきか?
大きなオブジェクトがあるものの、トップレベルのフィールドの変更のみを気にする場合は、浅いレスポンスを使用することでパフォーマンスを向上させることができます。
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef:Follow Only .value Replace All,Do not track internal properties
const bigData = shallowRef({ items: [...50000 items] })
bigData.value.items.push('new') // ❌ Do not trigger an update
bigData.value = { items: [newItems] } // ✅ Trigger an update(Replace All)
// shallowReactive:Track only top-level properties,No in-depth response
const state = shallowReactive({
user: { name: 'Alice' },
settings: { theme: 'dark' }
})
state.user.name = 'Bob' // ❌ Does not trigger
state.user = { name: 'Bob' } // ✅ Trigger(Top-Level Property Replacement)
(2) 更新を強制する
import { triggerRef } from 'vue'
const bigData = shallowRef({ items: [...] })
bigData.value.items.push('new') // Do not respond by default
triggerRef(bigData) // Manually Trigger an Update
代表的なシナリオ:大規模なテーブル(10万行)、大規模なリスト、および内部プロパティが頻繁に変更されるものの、重要なのは全体的な状態のみであるような状況。
8. toRef / toRefs:リアクティブ性を維持しつつデコンストラクションを行う
(1) toRefs: リアクティブオブジェクトのデストラクチャリング
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
name: 'Alice'
})
// ❌ General Deconstruction:Missing Response
const { count, name } = state
// ✅ toRefs Deconstruction:Stay Responsive
const { count, name } = toRefs(state)
// Used in templates
// <p>{{ count }}</p> <!-- Still responding -->
(2) toRef:通常の値をrefに変換する
import { toRef, ref } from 'vue'
// Scene: Select one field from props
const props = defineProps({ count: Number })
// ❌ Directly Deconstructing Missing Responses
const { count } = props // count It's ordinary number
// ✅ toRef Packaging
const count = toRef(props, 'count') // count is ref(props.count)
// Usage
count.value++ // Responsive Changes props
9. レスポンシブデザインの5つの落とし穴
(1) 落とし穴 1:反応性オブジェクトを直接置き換えること
let state = reactive({ count: 0 })
// ❌ state The entire variable has been changed,But the template uses the old version state
state = reactive({ count: 1 })
// ✅ Edit Properties
state.count = 1
(2) 落とし穴その2:反応的態度を分解し、反応そのものを失うこと
const state = reactive({ count: 0 })
// ❌ count It is a regular variable
const { count } = state
// ✅ Use toRefs
const { count } = toRefs(state)
(3) 落とし穴 3:配列への直接代入
const items = ref([1, 2, 3])
// ❌ Directly push won't react (need triggerRef)
items.value.push(4)
// ✅ Replace All
items.value = [...items.value, 4]
(4) 落とし穴 4:ref でラップされたオブジェクトの内容を直接変更すること
// It generally does not expire.,However, there are edge cases
const user = ref({ name: 'Alice' })
user.value.name = 'Bob' // ✅ Response(ref Wrapped objects are automatically unwrapped)
(5) 落とし穴 5:ref 型と reactive 型の混同
// ❌ Type error:reactive Cannot wrap the original value
const count = reactive(0) // Error: value cannot be made reactive
// ✅ Use ref
const count = ref(0)
10. 完全な例:レスポンシブ対応のショッピングカート
(1) ▶ サンプル:. ref の基本的な使い方
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++ // JS Must be used in .value
}
function reset() {
count.value = 0
}
</script>
(2) ▶ サンプル:. リアクティブ・ラッパーオブジェクト
<template>
<div>
<p>User: {{ state.user.name }}</p>
<p>Age: {{ state.user.age }}</p>
<button @click="birthday">+1 Year</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
user: { name: 'Alice', age: 25 }
})
function birthday() {
state.user.age++ // Edit directly,Automatic Response
}
</script>
(3) ▶ サンプル:. ref と reactive の比較
import { ref, reactive } from 'vue'
// ref Style
const userRef = ref({ name: 'Alice', age: 25 })
// Edit:
userRef.value.name = 'Bob' // ✅ Response
userRef.value = { name: 'Charlie', age: 30 } // ✅ Response
// reactive Style
const userReactive = reactive({ name: 'Alice', age: 25 })
// Edit:
userReactive.name = 'Bob' // ✅ Response
// userReactive = reactive({...}) // ❌ Failure
(4) ▶ サンプル:. toRefs—レスポンスを保持したままのデストラクチャリング
<template>
<div>
<p>{{ count }} - {{ name }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state) // Stay Responsive
function increment() {
count.value++ // count is ref, So use .value
}
</script>
(5) ▶ サンプル:. 浅いレスポンス + triggerRef
import { shallowRef, triggerRef } from 'vue'
// Large Table 100,000 rows → use shallowRef
const bigTable = shallowRef({
rows: Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Row ${i}` }))
})
// Replace All(Highly efficient)
function reloadData(newRows) {
bigTable.value = { rows: newRows }
}
// Modify Internal Properties(Not responding,Must be done manually trigger)
function updateRow(id, newName) {
const row = bigTable.value.rows.find(r => r.id === id)
if (row) {
row.name = newName
triggerRef(bigTable) // Manually Trigger an Update
}
}
(6) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
let state = reactive({...}); state = {...} |
状態全体を置き換える;テンプレートは更新されない | プロパティ state.x = 1 を変更 |
const { count } = state 分解 |
count を通常の変数に変換 |
toRefs(state) を使用 |
reactive(0) 元のパッケージの値 |
エラー | 元の値には ref() を使用してください |
count.value++ がテンプレート内にある |
エラー | テンプレート内に直接 {{ count }} がある |
array[index] = x 直接割り当て |
応答しない場合がある | splice を使用するか、セクション全体を置き換えてください |
❓ よくある質問
ref と reactive、どちらを選べばいいですか?ref を独占的に使用することです(理由:ref はあらゆる型に対応しており、プロパティや関数の置換・受け渡し全体がより明確になるため)。reactiveは、「状態が複数の関連するフィールドで構成される」シナリオ(Vuexなど)に適しています。初心者はまずrefを学び、習熟してからreactiveを使用することをお勧めします。.value が必要なのに、テンプレートには必要ないのですか?.value は、JavaScript エンジンに対して「これは ref でラップされた値であり、リアクティブにする必要がある」と明示的に伝えるために使用されます。Vue はテンプレートを独自に解析するため、どの要素が ref であるかを認識し、自動的にラップを解除します。要するに、JavaScript では(曖昧さを避けるために).value が必要ですが、テンプレートでは(利便性のために)ref が自動的にアンラップされるのです。.value.x を使用する必要がありますか?ref({name: 'Alice'}) の後、テンプレートでは {{ user.name }} が直接使用され、JavaScript では user.value.name が使用されます。Vue は、ref でラップされたオブジェクトの内部プロパティへのアクセスを自動的に処理します。toRefs() で囲んでください。const { count, name } = toRefs(state) の後、count と name は ref になりますが、テンプレート内の {{ count }} と JavaScript 内の count.value はどちらもリアクティブ性を維持します。refは深参照(内部プロパティの変更も反映されます)であるのに対し、shallowRefは.value全体の置換のみを追跡します(内部の変更は追跡しません)。データセットが巨大(10万件以上)で、全体的な置換のみを気にする場合は、shallowRef を使用する方がパフォーマンスが向上します。toRefs を使用せずにリアクティブをデストラクチャリングすること; (3) プリミティブ値を、リアクティブでない ref でラップすること(代わりに reactive を使用するか、ref を保持してください); (4) setupの外でrefを作成すること(これによりリアクティブ性が失われる);(5) 配列への直接代入(代わりにspliceを使用してください)。reactive は Map と Set をラップできますか?Proxy を使用して実装されている Vue 3 の reactive は、Map、Set、WeakMap、および WeakSet をサポートしています。Vue 2 ではこれはサポートされていません。📖 まとめ
- Vue 3のリアクティブシステムの中核:プロキシによるインターセプト + 依存関係の追跡 + 更新のトリガー
ref()任意の値をカプセル化します。JavaScriptからアクセスするには.valueが必要です。テンプレートによって自動的にアンラップされます。reactive()ラップされたオブジェクト(オブジェクト/配列)、ディープレスポンス、.value なしrefとreactiveのどちらを選ぶか:refは汎用性が高く、推奨されます。reactiveは「マルチフィールド状態」に適しています。- 5つの主な落とし穴:リアクティブオブジェクトの置換/デストラクチャリング中のレスポンスの喪失/プリミティブ値でのリアクティブの使用/配列への直接代入/型の混同
- shallowRef / shallowReactive は、大きなオブジェクトを扱う際のパフォーマンス最適化に使用されます
- toRefs はデストラクチャリング時にレスポンスを保持します。toRef は通常の値を ref に変換します。
📝 練習問題
(1) 基礎問題(難易度:⭐)
ref を使用して、簡単なショッピングカートの数量カウンターを作成してください:
- 初期値 1
- 「+」ボタン:1を加える
- 「-」ボタンを押すと、値が1減ります
- 数量が 0 以下の場合、「-」ボタンを無効にする (:disabled dynamic)
- 数量が 99 以上の場合、「+」ボタンを無効にする
(2) 上級問題(難易度:⭐⭐)
Reactive を使用して、ユーザープロフィールの編集フォームを作成してください:
- 「state」フィールドには、
name、email、ageの3つのフィールドが含まれています。 - 3つの入力フィールドに対する双方向バインディング(@inputイベントを使用。v-modelについては次のレッスンで解説します)
- 「リセット」ボタンを押すと、デフォルト値に戻ります
- 「送信」ボタンをクリックすると、完全な状態がコンソールに出力されます
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全なToDoアプリの実装(データ層):
todos配列をreactiveで包む- 各ToDoには
{ id, text, done }が含まれています - 提供されているメソッド:addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
- 以下の計算プロパティを提供してください:activeCount(未完了項目の数)および doneCount(完了項目の数)
- テンプレートに、ToDoリストの全項目と件数を表示する
初期データ:{ id: 1, text: 'Learn Vue 3', done: false }



