できない.dev

Vue 3 の Composition API で reactive を分割代入してリアクティビティが失われる

reactive() で作ったオブジェクトを分割代入や引数渡しすると Proxy 経由のアクセスが切れてリアクティブでなくなる。
toRefs() で個別の Ref に展開するか、プリミティブは ref() を使う。

#vue3#composition-api#reactive#ref#toRefs

公開:

要約

reactive() が返すのは Vue が生成した Proxy オブジェクトで、リアクティビティは Proxy 経由のプロパティアクセスに依存している。const { count } = state のように分割代入すると Proxy を経由しない素の値が取り出され、以降の変更が UI に反映されない。toRefs() で個別の Ref に変換するか、最初から ref() を使うのが正解。

よくある原因

  1. reactive({ count: 0 }) の戻り値を const { count } = state で分割代入し、テンプレートに count を直接渡している
  2. reactive(0) のようにプリミティブを直接渡している(公式 API リファレンスreactive がオブジェクトのみを受け付けると明記)
  3. コンポーザブルが返した reactive オブジェクトを受け取り側で const value = state.count のようにスナップショットしている
  4. watch(() => state.x, ...) の代わりに watch(state.x, ...) を書いており、依存追跡が効いていない

解決策

1. toRefs で分割代入する

import { reactive, toRefs } from 'vue'
 
const state = reactive({ count: 0, name: 'foo' })
const { count, name } = toRefs(state)
// count.value で読み書き。テンプレートでは {{ count }} で自動アンラップ

toRefs は各プロパティを Ref に変換するため、Proxy を経由したアクセスが維持される。

2. プリミティブには ref を使う

import { ref } from 'vue'
 
const count = ref(0)
function inc() {
  count.value++
}

ref はオブジェクトでラップして .value 経由でアクセスさせる仕組み。
プリミティブはこちらを基本とする。

3. watch / computed の依存はゲッタで渡す

import { reactive, watch, computed } from 'vue'
 
const state = reactive({ count: 0 })
 
watch(() => state.count, (n) => console.log(n))
const doubled = computed(() => state.count * 2)

第一引数を関数(ゲッタ)にすると、Vue が依存追跡を行える。watch(state.count, ...) は値そのものを渡しており追跡できない。

4. 受け渡しの境界では Ref に変換する

import { reactive, toRef } from 'vue'
 
const state = reactive({ user: { name: 'foo' } })
useLogger(toRef(state, 'user'))
// useLogger 側は Ref<User> として受け取れる

コンポーザブルや関数の境界でリアクティビティを保ちたいときは toRef / toRefs で個別の Ref に変換してから渡す。

この記事は役立ちましたか?