[Vue入門]リアクティビティの基礎

リアクティブな状態を宣言する

リアクティブなオブジェクトや配列を作るには、reactive() 関数を使用します。

import { reactive } from 'vue'

const state = reactive({ count: 0 })

リアクティブなオブジェクトは JavaScript プロキシ で、通常のオブジェクトと同じように振る舞います。違いは、Vue がリアクティブなオブジェクトのプロパティアクセスと変更を追跡できることです。詳細については、Reactivity in Depth で Vue のリアクティブシステムの仕組みを説明していますが、このメインガイドを読み終えた後に読むことをお勧めします。

コンポーネントのテンプレートでリアクティブな状態を使うには、下記に示すように、コンポーネントの setup() 関数で宣言し、それを返します:

import { reactive } from 'vue'

export default {
  // `setup` 関数は、Composition API 専用の特別なフックです。
  setup() {
    const state = reactive({ count: 0 })

    // 状態をテンプレートに公開します
    return {
      state
    }
  }
}
<div>{{ state.count }}</div>

同様に、リアクティブな状態を変化させる関数を同じスコープで宣言し、状態と並行してメソッドとして公開することができます:

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    function increment() {
      state.count++
    }

    // 関数も公開することを忘れないでください。
    return {
      state,
      increment
    }
  }
}

通常、公開されたメソッドはイベントリスナーとして使用されます。

<button @click="increment">
  {{ state.count }}
</button>

<script setup>

setup() 関数を使って手動で状態やメソッドを公開すると、冗長になることがあります。幸いなことに、これはビルドステップを使用しない場合にのみ必要です。単一ファイルコンポーネント (SFC) を使用する場合は、 <script setup> を使用することで大幅に簡略化することができます。

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

プレイグラウンドで試す

トップレベルのインポートと <script setup> で宣言された変数は、同じコンポーネントのテンプレートで自動的に使用できるようになります。

当ページ残りの部分では、Composition API のコード例として主に SFC + <script setup> という構文を使用します。

DOM 更新のタイミング

リアクティブな状態を変化させると、DOM は自動的に更新されます。しかし、DOM の更新は同期的に適用されないことに注意する必要があります。その代わりに Vue は、更新サイクルの「next tick」まで更新をバッファリングし、どれだけ状態を変化させても、各コンポーネントは一度だけ更新する必要があることを保証しています。

状態変化後の DOM 更新が完了するのを待つため、nextTick() というグローバル API を使用することができます:

import { nextTick } from 'vue'

function increment() {
  state.count++
  nextTick(() => {
    // DOM 更新にアクセスします
  })
}

ディープなリアクティビティ

Vue では、デフォルトで状態がリアクティブになっています。つまり、ネストしたオブジェクトや配列を変化させた場合でも、変更が検出されることが期待できます:

import { reactive } from 'vue'

const obj = reactive({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // これらは期待通りに動作します。
  obj.nested.count++
  obj.arr.push('baz')
}

また、ルートレベルでのみリアクティビティを追跡する shallow reactive object を明示的に作成することも可能ですが、これらは一般的に高度な使用例においてのみ必要とされるものとなります。

リアクティブプロキシ vs. 独自

注意すべきは、reactive() の戻り値が、元のオブジェクトの プロキシ であり、元のオブジェクトと等しくないということです:

const raw = {}
const proxy = reactive(raw)

// プロキシはオリジナルと同じではありません。
console.log(proxy === raw) // false

プロキシだけがリアクティブとなります。元のオブジェクトを変更しても更新は行われません。したがって、Vue のリアクティブシステムを使用する際のベストプラクティスは、プロキシされた状態のバージョンだけを使用することになります

プロキシへの一貫したアクセスを保証するために、同じオブジェクトに対して reactive() を呼ぶと常に同じプロキシを返し、既存のプロキシに対して reactive() を呼ぶとその同じプロキシも返されます。

// 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

このルールは、ネストされたオブジェクトにも適用されます。深いリアクティビティを持つため、リアクティブなオブジェクトの中にあるネストされたオブジェクトもプロキシとなります。

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

reactive() の制限

reactive() API には 2 つの制限があります:

  1. オブジェクト型 (オブジェクト、配列、および Map や Set などの コレクション型) に対してのみ機能します。文字列、数値、ブールなどの プリミティブ型 を保持することはできません。
  2. Vue のリアクティビティ追跡はプロパティアクセス上で動作するため、リアクティブなオブジェクトへの参照を常に同じに保つ必要があります。つまり、最初の参照へのリアクティブな接続が失われるため、リアクティブなオブジェクトを簡単に「置き換える」ことはできません:let state = reactive({ count: 0 }) // 上記の参照({ count: 0 })は、もはや追跡されていません(リアクティブな接続が失われました!) state = reactive({ count: 1 }) また、リアクティブなオブジェクトのプロパティをローカル変数に代入したり、分割代入したり、そのプロパティを関数に渡したりすると、下記に示すようにリアクティブなつながりが失われることとなります:const state = reactive({ count: 0 }) // n は切り離されたローカル変数 // を state.count から取得します。 let n = state.count // 元の状態に戻りません。 n++ // count も state.count と切り離されます。 let { count } = state // 元の状態に戻りません。 count++ // この関数が受け取る平文番号と // state.count の変更を追跡することができません。 callSomeFunction(state.count)

ref() と共に使うリアクティブな変数

Vue は、reactive() の制限に対処するため、ref() という関数も提供しており、任意の値の型を保持できるリアクティブな “refs “ を作成することができます:

import { ref } from 'vue'

const count = ref(0)

ref() は引数を受け取り、それを .value プロパティを持つ ref オブジェクトにラップして返します:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

リアクティブなオブジェクトのプロパティと同様に、ref の .value プロパティはリアクティブとなります。また、オブジェクト型を保持する場合、ref は .value を reactive() で自動的に変換します。

オブジェクトの値を含む ref は、オブジェクト全体をリアクティブに置き換えることができます:

const objectRef = ref({ count: 0 })

// これはリアクティブに動きます。
objectRef.value = { count: 1 }

また、Ref を関数に渡したり、プレーンオブジェクトから分解したりしても、リアクティビティが失われることはありません。

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// ref を受け取るこの関数は、
// .value を介して値にアクセスする必要がありますが、それは
// リアクティビティを保持します。
callSomeFunction(obj.foo)

// リアクティビティを保持しています。
const { foo, bar } = obj

つまり、ref() を使うと、任意の値への「参照」を作り、リアクティビティを失わずに受け渡しすることができます。この能力は、ロジックを Composable Functions に抽出する際に頻繁に使用されるため、非常に重要となります。

Ref Unwrapping in Templates

ref がテンプレートのトップレベルのプロパティとしてアクセスされた場合、それらは自動的に「アンラップ」されるので、.value を使用する必要はありません。以下は、先ほどのカウンターの例で、代わりに ref() を使用したものとなります:

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

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- .value は必要ありません -->
  </button>
</template>

プレイグラウンドで試す

アンラップは、ref がテンプレートに描画されるコンテキスト上のトップレベルのプロパティである場合にのみ適用されることに注意してください。例として foo はトップレベルのプロパティですが、object.foo はトップレベルではありません。

そこで、下記に示したようなオブジェクトが与えられた:

const object = { foo: ref(1) }

下記に示した式は、期待通りに動作 しません :

{{ object.foo + 1 }}

レンダリング結果は [object Object] となります。これは object.foo が ref オブジェクトであるためです。これを解決するには、下記に示すように foo をトップレベルのプロパティにします:

const { foo } = object
{{ foo + 1 }}

これで、レンダリング結果は「2」になります。

注意点としては、ref がテキスト補間の最終評価値(つまり {{ }} タグ)である場合もアンラップされるので、以下のように 1 がレンダリングされます。

{{ object.foo }}

これはテキスト補間の便利な機能に過ぎず、 {{ object.foo.value }} と等価になります。

リアクティブなオブジェクトにおける Ref のアンラッピング

リアクティブなオブジェクトのプロパティとして ref にアクセスしたり変化させたりすると、自動的にアンラップされるので、通常のプロパティと同じように振る舞うことができます:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

既存の ref にリンクされたプロパティに新しい ref が割り当てられた場合、下記に示すように、それは古い ref を置き換えることとなります:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 元の ref は state.count から切り離されました。
console.log(count.value) // 1

Ref のアンラッピングは、より深いリアクティブなオブジェクトの内部にネストされている場合にのみ発生します。浅いリアクティブなオブジェクト のプロパティとしてアクセスされた場合は適用されません。

配列とコレクションにおける Ref のアンラッピング

リアクティブなオブジェクトと異なり、ref がリアクティブな配列の要素や、Map のようなネイティブコレクション型としてアクセスされた場合には、アンラップは行われません。

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)

Reactivity Transform 

Ref で .value を使わなければならないのは、JavaScript の言語的な制約による欠点です。しかし、コンパイル時の変換 (ここでいうコンパイル時とは SFC を JavaScript コードへ変換する時) を利用すれば、適切な場所に自動的に .value を追加して人間工学を改善することができます。Vue はコンパイル時の変換を提供しており、先ほどの「カウンター」の例をこのように記述することができます。

<script setup>
let count = $ref(0)

function increment() {
  // ここでは .value が不要です
  count++
}
</script>

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

Reactivity Transform の詳細については、専用のセクションで説明されています。ただし、現在はまだ実験的なものであり、最終的に完成するまでに変更される可能性があることに注意してください。