5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vueでリアクティブ性が失われない分割代入

Last updated at Posted at 2023-12-28

まだ追記予定ですが、いつになるかわからないのでとりあえず公開

概要

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の変数参照の仕組みによるものなのでそのあたりを調べるとより深く知ることができてリアクティブを活かせるかも

JavaScriptに参照渡し/値渡しなど存在しない

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?