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() を使うのが正解。
よくある原因
reactive({ count: 0 })の戻り値をconst { count } = stateで分割代入し、テンプレートにcountを直接渡しているreactive(0)のようにプリミティブを直接渡している(公式 API リファレンス はreactiveがオブジェクトのみを受け付けると明記)- コンポーザブルが返した reactive オブジェクトを受け取り側で
const value = state.countのようにスナップショットしている 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 に変換してから渡す。