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

<!-- 分割代入するとリアクティブが失われると言われるが、残る場合がある（ネストオブジェクト）

そのままオブジェクト配列・オブジェクトMapなどの言及につなげる
-->

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

## 概要

Vue3.x Composition APIにて使用される`ref()`や`reactive()`といったリアクティブな変数を定義する機能があります。よく言われるのは**オブジェクト型リアクティブに分割代入をするとリアクティブ性が失われる**といった話がありますが、実はそうでない場合が簡単に起こりうることが分かったので書いてみようと思います

## 読者対象

・TS・JSでのある程度の変数型を使ったことがある人（特に配列やMap）
・Vue3.xのComposition APIを触ったことがある人
・Vueのリアクティブを深く理解したい人

## 基礎知識：`ref()`と`reactive()`の原理

まずはVueでのリアクティブのおさらいです
Composition APIではリアクティブな変数を利用するために
- `ref()`
- `reactive()`
の２つを主に使います。公式リファレンスは[こちら](https://ja.vuejs.org/api/reactivity-core)

比較表にすると以下のようになります
|比較内容|`ref()`|`reactive()`|
|---|---|---|
|利用できる型|オブジェクト型orプリミティブ型|オブジェクト型|
|参照方法|`.value`プロパティ|そのまま|
|検知方式|ゲッタ・セッタ|Proxy|

特に重要なのはリアクティブは変更検知するものなので、**検知方式**を理解しておくために解説をしてみます

### `ref()`（ゲッタ・セッタ方式）

`.value`へのゲッタ・セッタを利用して変更を検知しています
オーソドックスな変更検知方式といえますが、`.value`から必ず参照する必要があります
また、`ref()`で作ったリアクティブな変数は`Ref型`と呼ばれます

:::note warn
補足
上記表でもあるとおり、実は`ref()`でもオブジェクト型が使用できます。理由としては`.value`自体が`reactive()`したオブジェクトと同じものになるためです
:::

### `reactive()`（Proxy方式）

[`ES Proxy`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy)を利用してオブジェクトのプロパティアクセス時に変更検知しています
型からリアクティブかどうかはわからず単純にそのオブジェクト型になるため、代入時など注意が必要です
```ts
let obj = reactive({
    name: "John",
    age: 24,
})

// プロパティに代入すると変更検知する
obj.name = "Sam"

// 代入できてしまうが、ここでリアクティブ性は失われる(Proxyではなくなる）
obj = {...obj, name: "John"}
```

:::note info
Proxyかどうかはconsole出力等すると一応確認できます
:::

`reactive()`はこのように **`.value`を使用しなくてもリアクティブな変数が使えるという利点**があります。また、以下もリアクティブにすることができます
- 配列やMap
- 深いオブジェクト（ネストしたもの）
ネストしたオブジェクトはProxyに自動変換されます

:::note info
なぜ`ref()`,`reactive()`のように二種あるのかの補足情報（わかる人向け）
JSの特性を考えるとわかりやすいです
- **プリミティブ（ref担当）**: イミュータブルなので変更のたびに参照先がかわる
- **オブジェクト（reactive担当）**: ミュータブルなのでプロパティを変更してもオブジェクト自体の参照先はかわらない

これらの変更検知をするためだと思われます
:::

### 使い分けについて

個人的には以下の理由により`ref()`がおすすめです
- `Proxy型`はTSでも型検知がされず、何がリアクティブになっているのかわかりにくい
- `.value`があるのでリアクティブであることがわかる
- オブジェクトでも使える
- `reactive()`ではプリミティブは使えない

そのためここからはできるだけ`reactive()`を使わない方針にしているのでご了承ください

## リアクティブ性が失われない分割代入について

ここから本題のリアクティブの分割代入の動作です
Vueではよく「**リアクティブにした変数の分割代入はリアクティブ性が失われる**」とありますが、**そうでない場合がある**ので、上にて説明したリアクティブの仕組みをもとに解説します


### リアクティブが失われる場合

このようなオブジェクトの分割代入では公式の説明の通りリアクティブにはなりません
細かくいうと**変更検知する仕組みが失われるため** です

```ts
const obj = reactive({
    a: 1,
    b: "hoge"
})

const { a } = obj  // 分割代入時にただの変数になる
obj.a = 2

console.log(a) // 1
```

#### 解決法
リアクティブにしたオブジェクトに対して`toRefs`を使うことで`ref()`をした形(`Ref型`)になります

```ts
const obj = reactive({
    a: 1,
    b: "hoge"
})

const { a } = toRefs(obj.value) // 分割代入時にリアクティブ(Ref型)になる
obj.a = 2

console.log(a.value)  // 2
```

### リアクティブが残る場合

上がよくある例ですが、実は**ネストしたオブジェクトを分割代入するとリアクティブが保たれます**
これはネストされたオブジェクトも`reactive()`した`Proxy`型になっているためです

```ts
const obj = reactive({
    a: {
        aa: true,
        bb: "huge",
    },
    b: "hoge",
})

const { a } = obj
console.log(isReactive(a)) // true
```

:::note info
`isReactive()`とは
`isReactive()`を使うと`reactive()`(Proxy型)のものが判断できます（[公式URL](https://ja.vuejs.org/api/reactivity-utilities#isreactive)）
:::

わかりにくいので分割代入時には`toRefs()`でRef型にすることがベストかなと思います
```ts
const { a } = toRefs(obj.value)
// or const a = toRef(obj.value.a)
console.log(isReactive(a.value)) // true
```

:::info
補足ですが、コンポジションだと内部での状態更新は`reactive()`で扱って公開するときには`toRefs()`することが多い印象です
```ts
consy useUserState = () => {
    const state = reactive({
        user: User,
        isLoading: boolean,
        ...
    })
    return {
        ...toRefs(state),
    }
}

// 使用側
const {user, isLoading} = useUserState()

```
こうすることで内部では扱いやすい形にして、外部では分割代入で使用しやすいRef型で取得することができます

:::

そして実はこのことより **「オブジェクト型の配列」や「オブジェクト型のMap」の要素取得も実は同じリアクティブ性を守った分割代入**と捉えることができます

:::note info
配列やMapは元はオブジェクト型なので`reactive()`が適応されます
:::

#### オブジェクト配列型でのリアクティブ

配列だと以下のようにリアクティブを保って参照できます
```ts
const Users = ref<User[]>([...])

isReactive(users.value)  // true
isReactive(users.value[0])  // true
```

そのため、他変数に代入（つまりは分割代入）してもリアクティブが守られます
```ts
users.value[0].name  // "John"

const user1 = users.value[0]  // 代入（分割代入と同等）
user1.name = "Sam"
users.value[0].name  // "Sam"
```

急にrefからreactiveになるのでわかりにくい感があります。実際に配列の一部をリアクティブ保ったまま取るならば`toRef`を使うのが良いでしょう
```ts
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の変数参照の仕組みによるものなのでそのあたりを調べるとより深く知ることができてリアクティブを活かせるかも

[JavaScriptに参照渡し/値渡しなど存在しない](https://qiita.com/yuta0801/items/f8690a6e129c594de5fb)