まだ追記予定ですが、いつになるかわからないのでとりあえず公開
概要
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へのゲッタ・セッタを利用して変更を検知しています
オーソドックスな変更検知方式といえますが、.valueから必ず参照する必要があります
また、ref()で作ったリアクティブな変数はRef型と呼ばれます
補足
上記表でもあるとおり、実はref()でもオブジェクト型が使用できます。理由としては.value自体がreactive()したオブジェクトと同じものになるためです
reactive()(Proxy方式)
ES Proxyを利用してオブジェクトのプロパティアクセス時に変更検知しています
型からリアクティブかどうかはわからず単純にそのオブジェクト型になるため、代入時など注意が必要です
let obj = reactive({
name: "John",
age: 24,
})
// プロパティに代入すると変更検知する
obj.name = "Sam"
// 代入できてしまうが、ここでリアクティブ性は失われる(Proxyではなくなる)
obj = {...obj, name: "John"}
Proxyかどうかはconsole出力等すると一応確認できます
reactive()はこのように .valueを使用しなくてもリアクティブな変数が使えるという利点があります。また、以下もリアクティブにすることができます
- 配列やMap
- 深いオブジェクト(ネストしたもの)
ネストしたオブジェクトはProxyに自動変換されます
なぜref(),reactive()のように二種あるのかの補足情報(わかる人向け)
JSの特性を考えるとわかりやすいです
- プリミティブ(ref担当): イミュータブルなので変更のたびに参照先がかわる
- オブジェクト(reactive担当): ミュータブルなのでプロパティを変更してもオブジェクト自体の参照先はかわらない
これらの変更検知をするためだと思われます
使い分けについて
個人的には以下の理由によりref()がおすすめです
-
Proxy型はTSでも型検知がされず、何がリアクティブになっているのかわかりにくい -
.valueがあるのでリアクティブであることがわかる - オブジェクトでも使える
-
reactive()ではプリミティブは使えない
そのためここからはできるだけreactive()を使わない方針にしているのでご了承ください
リアクティブ性が失われない分割代入について
ここから本題のリアクティブの分割代入の動作です
Vueではよく「リアクティブにした変数の分割代入はリアクティブ性が失われる」とありますが、そうでない場合があるので、上にて説明したリアクティブの仕組みをもとに解説します
リアクティブが失われる場合
このようなオブジェクトの分割代入では公式の説明の通りリアクティブにはなりません
細かくいうと変更検知する仕組みが失われるため です
const obj = reactive({
a: 1,
b: "hoge"
})
const { a } = obj // 分割代入時にただの変数になる
obj.a = 2
console.log(a) // 1
解決法
リアクティブにしたオブジェクトに対してtoRefsを使うことでref()をした形(Ref型)になります
const obj = reactive({
a: 1,
b: "hoge"
})
const { a } = toRefs(obj.value) // 分割代入時にリアクティブ(Ref型)になる
obj.a = 2
console.log(a.value) // 2
リアクティブが残る場合
上がよくある例ですが、実はネストしたオブジェクトを分割代入するとリアクティブが保たれます
これはネストされたオブジェクトもreactive()したProxy型になっているためです
const obj = reactive({
a: {
aa: true,
bb: "huge",
},
b: "hoge",
})
const { a } = obj
console.log(isReactive(a)) // true
isReactive()とは
isReactive()を使うとreactive()(Proxy型)のものが判断できます(公式URL)
わかりにくいので分割代入時にはtoRefs()でRef型にすることがベストかなと思います
const { a } = toRefs(obj.value)
// or const a = toRef(obj.value.a)
console.log(isReactive(a.value)) // true
補足ですが、コンポジションだと内部での状態更新はreactive()で扱って公開するときにはtoRefs()することが多い印象です
consy useUserState = () => {
const state = reactive({
user: User,
isLoading: boolean,
...
})
return {
...toRefs(state),
}
}
// 使用側
const {user, isLoading} = useUserState()
こうすることで内部では扱いやすい形にして、外部では分割代入で使用しやすいRef型で取得することができます
そして実はこのことより 「オブジェクト型の配列」や「オブジェクト型のMap」の要素取得も実は同じリアクティブ性を守った分割代入と捉えることができます
配列やMapは元はオブジェクト型なのでreactive()が適応されます
オブジェクト配列型でのリアクティブ
配列だと以下のようにリアクティブを保って参照できます
const Users = ref<User[]>([...])
isReactive(users.value) // true
isReactive(users.value[0]) // true
そのため、他変数に代入(つまりは分割代入)してもリアクティブが守られます
users.value[0].name // "John"
const user1 = users.value[0] // 代入(分割代入と同等)
user1.name = "Sam"
users.value[0].name // "Sam"
急にrefからreactiveになるのでわかりにくい感があります。実際に配列の一部をリアクティブ保ったまま取るならばtoRefを使うのが良いでしょう
users.value[0].name // "John"
const user1 = toRef(users.value[0]) // 代入(分割代入と同等)
user1.value.name = "Sam"
users.value[0].name // "Sam"
// 以下のように一気に代入もしやすい
user1.value = {...}
オブジェクトMap型でのリアクティブ
書く予定
補足
使えるかわからない補足事項
浅いリアクティブ
ref()でネストしたオブジェクトもリアクティブになりますが、そうしたくない場合はshallowRef()という関数で浅いリアクティブとして利用できます(.valueからのアクセスのみリアクティブになる)
当記事の深い理解がしたい方用
今回のようなリアクティブの有無はVueによるものというより、基本的にはJSの変数参照の仕組みによるものなのでそのあたりを調べるとより深く知ることができてリアクティブを活かせるかも