まだ追記予定ですが、いつになるかわからないのでとりあえず公開
概要
Vue3.x Composition APIにて使用されるref()
やreactive()
といったリアクティブな変数を定義する機能があります。よく言われるのはオブジェクト型リアクティブに分割代入をするとリアクティブ性が失われるといった話がありますが、実はそうでない場合が簡単に起こりうることが分かったので書いてみようと思います
読者対象
・TS・JSでのある程度の変数型を使ったことがある人(特に配列やMap)
・Vue3.xのComposition APIを触ったことがある人
・Vueのリアクティブを深く理解したい人
基礎知識:ref()
とreactive()
の原理
まずはVueでのリアクティブのおさらいです
Composition APIではリアクティブな変数を利用するために
ref()
-
reactive()
の2つを主に使います。公式リファレンスはこちら
比較表にすると以下のようになります
比較内容 | ref() |
reactive() |
---|---|---|
利用できる型 | オブジェクト型orプリミティブ型 | オブジェクト型 |
参照方法 |
.value プロパティ |
そのまま |
検知方式 | ゲッタ・セッタ | Proxy |
特に重要なのはリアクティブは変更検知するものなので、検知方式を理解しておくために解説をしてみます
ref()
(ゲッタ・セッタ方式)
ゲッタ・セッタを利用して.value
の変更を検知しています
オーソドックスな変更検知方式といえますが変数の直接代入しか検知できず、この方式ではオブジェクトのプロパティ等の変更検知はできないという欠点があります。
また、ref()
で作ったリアクティブな変数はRef型
と呼ばれます
補足
上記表ではref()
でもオブジェクト型が使えるとしたのは、オブジェクト型を含む変数は自動でreactive()
(Proxy方式)を適応するためです(詳細は以下)。
ただし、アクセス法は.value
からになります
reactive()
(Proxy方式)
ES Proxy
を利用してオブジェクトのプロパティアクセス時に変更検知しています
注意点としてProxy
なのでオブジェクト変数への直接代入ではなく、プロパティの変更のみを検知します
オブジェクト変数への直接代入とは
reactive()
でProxy型
にした変数にオブジェクト型
を代入することです
代入することでProxy型
ではなくなるのでリアクティブではなくなります
なので直接代入のプリミティブ型が使えませんが、そのかわり .value
を使用しなくてもリアクティブな変数が使えるという利点 があります
また、以下もJSではオブジェクトと同等なのでリアクティブにすることができます
- 配列やMap
- 深いオブジェクト(ネストしたもの)
ネストしたオブジェクトはProxyに自動変換されます
なぜref()
,reactive()
二種あるのかの補足情報(わかる人向け)
JSの特性を考えるとわかりやすいです
- プリミティブ: イミュータブルなので変更のたびに参照先がかわる
- オブジェクト: ミュータブルなのでプロパティを変更してもオブジェクト自体の参照先はかわらない
これらの変更検知のためだと思われます
使い分けについて
個人的には以下の理由によりref()
がおすすめです
-
Proxy型
はTSでも型検知がされず、何がリアクティブになっているのかわかりにくい -
.value
があるのでリアクティブであることがわかる -
reactive()
ではプリミティブは使えない
そのためここからはできるだけreactive()
を使わないようにしているのでご了承を
リアクティブ性が失われない分割代入について
ここから本題のリアクティブの分割代入の動作です
Vueではよくリアクティブにした変数の分割代入はリアクティブ性が失われるとありますが、そうでない場合があるので、上にて説明したリアクティブの仕組みをもとに解説します
リアクティブが失われるやり方
このようなオブジェクトの分割代入では公式の説明の通りリアクティブにはなりません
細かくいうと変更検知する仕組みが失われるため です
const obj = ref({
a: 1,
b: "hoge"
})
const { a } = obj.value // 分割代入時にただの変数になる
console.log(isReactive(a)) // false
解決法
リアクティブにしたオブジェクトに対してtoRefs
を使うことでref()
をした形(Ref型
)になります
const { a } = toRefs(obj.value) // 分割代入時にリアクティブ(Ref型)になる
console.log(isReactive(a.value)) // true
isReactive()
とは
isReactive()
を使うとreactive()
(Proxy型)のものが判断できます(公式URL)
なぜ.value
を判定しているのか
上での説明であったようにオブジェクトをref()
するとreactive()
が自動で適応されるので.value
自体がリアクティブになっています
リアクティブが残るやり方
上がよくある例ですが、実はネストしたオブジェクトを分割代入するとリアクティブが保たれます
const obj = ref({
a: {
aa: true,
bb: "huge",
},
b: "hoge",
})
const { a } = obj.value
console.log(isReactive(a)) // true
これはネストされたオブジェクトもreactive()
したProxy
型になっているためです
また、これだとリアクティブだとわかりにくいのでtoRefs()
でRef型にすると良いかもです
const { a } = toRefs(obj.value)
// or const a = toRef(obj.value.a)
console.log(isReactive(a.value)) // true
そして実はこのことよりオブジェクト型配列やオブジェクト型Mapの要素取得も実は同じリアクティブ性を守った分割代入と捉えることができます
配列やMapは元はオブジェクト型なのでreactive()
が適応されます
オブジェクト配列型でのリアクティブ
配列だと以下のようにリアクティブを保って参照できます
const objs = ref([...])
console.log(isReactive(objs.value)) // true
console.log(isReactive(objs.value[0])) // true
他変数に代入(つまりは分割代入)してもリアクティブが守られる
console.log(objs.value[0].a) // 0
const obj0 = objs.value[0] // 元の配列とリアクティブを保ったまま代入
obj0.a = 1
console.log(objs.value[0].a) // 1
ただし以下のような使い方は配列のリアクティブとの関連が消えるので注意
console.log(objs.value[0].a) // 0
const obj0 = objs.value[0] // 元の配列とリアクティブを保ったまま代入
objs0 = { a: 1, ... } // この代入では元配列とのリアクティブが失われる
console.log(objs.value[0].a) // 0
前者のようにプロパティに直接代入すればリアクティブが守られますが、複雑なオブジェクトほどたくさんあってめんどくさいので、以下のように書くとリアクティブを保ったまま一括代入ができます
Object.assign(objs.value[0], { a: 1, ... })
オブジェクトMap型でのリアクティブ
書く予定
補足
使えるかわからない補足事項
浅いリアクティブ
ref()
でネストしたオブジェクトもリアクティブになりますが、そうしたくない場合はshallowRef()
という関数で浅いリアクティブとして利用できます(.value
からのアクセスのみリアクティブになる)
当記事の深い理解がしたい方用
今回のようなリアクティブの有無はVueによるものというより、基本的にはJSの変数参照の仕組みによるものなのでそのあたりを調べるとより深く知ることができてリアクティブを活かせるかも