Vue3のStore（Pinia）のリアクティブ(reactive)の動きを確認してみた

## はじめに

リアクティブに関してのPinia Storeの動きが、Vue3のComposition APIの中でどうなるのか？Options APIの時はどう実装すべきか？混乱してしまい、良く分からなくなってしまったので、自身の整理メモとして備忘録を残しておこうと思う。

試したパターンは以下。

- storeのstateは**プリミティブな値**
  - Piniaを Composition API で実装
    - コンポーネント側もComposition API
    - 番外編　コンポーネント側はOptions API 
  - Piniaを Options API で実装
    - コンポーネント側もOptions API 
    - 番外編　コンポーネント側はComposition API
- storeのstateは**オブジェクト**
  - Piniaを Composition API で実装
    - コンポーネント側もComposition API
    - 番外編　コンポーネント側はOptions API
  - Piniaを Options API で実装
    - コンポーネント側もOptions API 
    - 番外編　コンポーネント側はComposition API

## storeのstateはプリミティブな値
### Composition API

PiniaのStoreの実装は以下とする。

```js
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';

export default defineStore('counter', () => {
	const count = ref(0);
	const doubleCount = computed(() => count.value * 2);
	const increment = () => {
		console.log('increment');
		count.value += 1;
		console.log(count.value);
	};
	return { count, doubleCount, increment };
});
```

#### reactiveになるパターン　use...Store()のstoreをそのまま利用

```vue
<script setup>
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ counterStore.count }}</div>
			<div>doubleCount : {{ counterStore.doubleCount }}</div>
			<v-btn @click="counterStore.increment()">counter up</v-btn>
		</v-container>
	</v-main>
</template>
```

この時、以下の動画の通り、counterはincrement()で+1されて、それがリアクティブに画面に反映される事が確認できる。まあこれはそうだよね、となると思う。
![ezgif.com-video-to-gif (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/21533941-b375-1ff6-78ca-7b9a9f711114.gif)

上記のコードだと、以下の部分が冗長なので分割代入をしたくなる、気がするが、それを次に試してみようと思う。
```vue
			<!-- counterStore.〇〇になっており冗長。単にcount, doubleCount, increment() と指定できるように分割代入をやってみると… -->
			<div>count : {{ counterStore.count }}</div>
			<div>doubleCount : {{ counterStore.doubleCount }}</div>
			<v-btn @click="counterStore.increment()">counter up</v-btn>
```

#### reactiveに「ならない」パターン　use...Store()の戻りを分割代入で利用

```vue
<script setup>
import useCounterStore from '@/stores/counter';

const { count, doubleCount, increment } = useCounterStore();
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
		</v-container>
	</v-main>
</template>
```

上記のように実装した場合、以下の動画の通り、リアクティブにはならずincrementをしても画面上の数字は0のままになってしまう。
![ezgif.com-video-to-gif (6).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/cd9b924c-6a33-14ed-a01f-44a70aa12947.gif)

どうしてこうなってしまうか？だが、順を追ってみていく。

まず、Piniaの[defineStore](https://pinia.vuejs.org/api/modules/pinia.html#Functions-defineStore)はストアインスタンスを返すが、そのインスタンスはリアクティブなオブジェクト（JavaScript プロキシ）である（[Vue におけるリアクティビティーの仕組み](https://ja.vuejs.org/guide/extras/reactivity-in-depth.html#how-reactivity-works-in-vue)も参照）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/19af651b-7f57-8172-35d3-858899cecea3.png)

リアクティブなオブジェクトであるが故に、以下の引用の通り、分割代入を行ってしまうとリアクティブな状態ではなくなってしまう、という性質を持つ。

> リアクティブオブジェクトのプロパティをローカル変数に割り当てたり分割代入した場合、ローカル変数へのアクセスはプロキシーが仕込んだ get/set をトリガーしなくなるため、リアクティビティーが"切断"されます。
> 引用元：[Vue におけるリアクティビティーの仕組み](https://ja.vuejs.org/guide/extras/reactivity-in-depth.html#how-reactivity-works-in-vue)

> また、リアクティブなオブジェクトのプロパティをローカル変数に代入したり、分割代入したり、そのプロパティを関数に渡したりすると、下記に示すようにリアクティブなつながりが失われることとなります
> 引用元：[reactive() の制限](https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive)

そのため、今回分割代入を行った事でリアクティビィティーが失われ、increment()でcountを+1しても画面上にはそれが反映されなかった。では、どうすればいいか？だが、それは次の項で見ていく。

※以下のようなコードを書くと、useCounterStore()がリアクティブなオブジェクトである事は分かる。以下は[リアクティブプロキシ vs. 独自](https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-proxy-vs-original-1)に書かれている通り、reactive()は既存のプロキシに対して reactive() を呼ぶとその同じプロキシが返される、という性質を利用して、useCounterStore()が返しているものがリアクティブなオブジェクト（プロキシ）である事を確認している。
```vue
<script setup>
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
console.log(reactive(counterStore) === counterStore); // true ← calling reactive() on a proxy returns itself
</script>
...
```

#### reactiveになるパターン　storeToRefsで各プロパティをrefに変換し分割代入を利用できるようにする

```vue
<script setup>
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { count, doubleCount } = storeToRefs(counterStore);
const { increment } = counterStore;
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
		</v-container>
	</v-main>
</template>
```

上記のように実装した場合、以下の動画の通り、リアクティブな状態が保たれ、incrementをするとcountが+1される事が確認できる。
![ezgif.com-video-to-gif (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/21533941-b375-1ff6-78ca-7b9a9f711114.gif)

`storeToRefs`が何者か？を見ていく。[API Reference](https://pinia.vuejs.org/api/modules/pinia.html#Functions-storeToRefs)によるとstoreToRefsは以下のような機能を持つ。

> Creates an object of references with all the state, getters, and plugin-added state properties of the store. Similar to toRefs() but specifically designed for Pinia stores so methods and non reactive properties are completely ignored.（ストアのすべての状態、ゲッター、およびプラグインで追加された状態プロパティを含む参照オブジェクトを作成します。toRefs() と似ていますが、特に Pinia ストア用に設計されているので、 メソッドや非反応プロパティは完全に無視されます。）

ここで、`toRefs()`というのが出てきたが、これは[API Reference](https://ja.vuejs.org/api/reactivity-utilities.html#toref)にあるようにリアクティブなオブジェクトの各プロパティをrefに変換する関数。そのため以下のように分割代入してもそれぞれがrefなのでリアクティブな状態が保たれたまま利用できる（`count.value`のようにしなければならないのは、[API Reference](https://ja.vuejs.org/api/reactivity-core.html#ref)や[]()にあるように、ref()の戻りはrefオブジェクトであり、その`value`プロパティがリアクティブであるという仕様になっているため）。
```vue
<script setup>
import { toRefs } from 'vue';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { count } = toRefs(counterStore);
console.log(count); // 以下のスクショのようにObjectRef
console.log(count.value); // 0
</script>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/8021f8fa-e657-bc50-9c06-2c49b1d04fc1.png)

ただ、`toRefs()`はリアクティブなオブジェクトのプロパティを何でもかんでもrefに変えてしまうので、refに変える対象をstateとgettersにするための特別なtoRefs()相当の関数が`storeToRefs()`という関数の正体。なので、以下のようにconsoleで各プロパティを確認してみると、以下のスクショのようにそれぞれRef、ComputedRefの参照を持っている事が確認できる。
```vue
<script setup>
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { count, doubleCount } = storeToRefs(counterStore);
console.log(count);
console.log(doubleCount);
</script>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a9e7560d-31bf-e6fe-f584-40288988558d.png)

※この項の最初に示した実装で、テンプレート内で`{{ count }}`や`{{ doubleCount }}`と実装しており、`{{ count.value }}`になっていなかった。これは[Ref Unwrapping in Templates](https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#ref-unwrapping-in-templates)に書かれている通り、自動でrefの`value`プロパティを設定していると解釈してくれるため（以下、公式からの引用）。

> ref がテンプレートのトップレベルのプロパティとしてアクセスされた場合、それらは自動的に「アンラップ」される

JavaScript内で利用する場合には、以下のように`.value`にアクセスする必要があるので注意。
```vue
<script setup>
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { count, doubleCount } = storeToRefs(counterStore);
...
console.log(count.value); // 0
</script>
```

#### 【番外編】コンポーネント側をOptions APIで実装する場合

以下のいずれでも動きは上記のComposition APIの時と同じ。

- 参考：https://pinia.vuejs.org/core-concepts/getters.html#with-setup

##### storeToRefsを利用する場合

```vue
<script>
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { count, doubleCount } = storeToRefs(counterStore);
		const { increment } = counterStore;

		return { count, doubleCount, increment };
	},
	created() {
		console.log(this.count); // 0
		console.log(this.doubleCount); // 0
	}
};
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
		</v-container>
	</v-main>
</template>
```

##### storeToRefsを利用しない場合

```vue
<script>
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { increment } = counterStore;

		return { counterStore, increment };
	},
	computed: {
		count() {
			return this.counterStore.count;
		},
		doubleCount() {
			return this.counterStore.doubleCount;
		}
	},
	created() {
		console.log(this.count);
		console.log(this.doubleCount);
	}
};
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
		</v-container>
	</v-main>
</template>
```

※以下のような実装では分割代入でリアクティビティーが失われるので期待通りに動作しない（incrementをしても画面のcountは0のままになる）
```vue
<script>
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { count, doubleCount, increment } = counterStore;

		return { count, doubleCount, increment };
	},
	created() {
		console.log(this.count);
		console.log(this.doubleCount);
	}
};
</script>

<template>
	省略
</template>
```

### Options API

PiniaのStoreの実装は以下とする。

```js
import { defineStore } from 'pinia';

export default defineStore('counter', {
	state: () => ({ count: 0 }),
	getters: {
		doubleCount: (state) => state.count * 2
	},
	actions: {
		increment() {
			console.log('increment');
			this.count += 1;
			console.log(this.count);
		}
	}
});
```

#### reactiveになるパターン　use...Store()のstoreをそのまま利用

[#storetorefsを利用しない場合](#storetorefsを利用しない場合)の実装と全く同じ。もちろん動作は期待通り（incrementごとにcountが+1される）。

※count, doubleCountを分割代入する実装（以下）だと、[#storetorefsを利用しない場合](#storetorefsを利用しない場合)で触れたように期待通りの動作にならない。
```vue
<script>
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { count, doubleCount, increment } = counterStore;

		return { count, doubleCount, increment };
	},
	...
};
</script>
```

#### reactiveになるパターン　storeToRefsで各プロパティをrefに変換し分割代入を利用できるようにする

[storeToRefsを利用する場合](#storetorefsを利用する場合)の実装と全く同じ。

#### 【番外編】コンポーネント側をComposition APIで実装する場合

[reactiveになるパターン　use...Store()のstoreをそのまま利用](#reactiveになるパターンusestoreのstoreをそのまま利用)や[reactiveになるパターン　storeToRefsで各プロパティをrefに変換し分割代入を利用できるようにする](#reactiveになるパターンstoretorefsで各プロパティをrefに変換し分割代入を利用できるようにする)の実装と全く同じ。

## storeのstateはオブジェクト

### Composition API

PiniaのStoreの実装は以下とする。
```js
import { computed, ref, toRefs } from 'vue';
import { defineStore } from 'pinia';

export default defineStore('counter', () => {
	const pageRef = ref({ count: 0, list: ['hoge', 'foo'] });
	const doubleCount = computed(() => pageRef.value.count * 2);
	const page = computed(() => pageRef.value);
	const increment = () => {
		console.log('increment');
		pageRef.value.count += 1;
		console.log(pageRef.value.count);
	};
	const pushList = () => {
		pageRef.value.list.push('test');
	};

	return { pageRef, page, doubleCount, increment, pushList };
});
```

#### reactiveになるパターン　use...Store()のstoreをそのまま利用

[【番外編】コンポーネント側をComposition APIで実装する場合の「reactiveになるパターン　use...Store()のstoreをそのまま利用」](#reactiveになるパターンusestoreのstoreをそのまま利用-3)と全く同じ。

#### reactiveになるパターン　storeToRefsで各プロパティをrefに変換し分割代入を利用できるようにする

[【番外編】コンポーネント側をComposition APIで実装する場合の「reactiveになるパターン　storeToRefsで各プロパティをrefに変換し分割代入を利用できるようにする」](#reactiveになるパターンstoretorefsで各プロパティをrefに変換し分割代入を利用できるようにする-3)と全く同じ。

※[reactiveになるパターン②　toRefs()を利用してreactive()をプレーンオブジェクトに変換に変換](#reactiveになるパターンtorefsを利用してreactiveをプレーンオブジェクトに変換に変換)の実装方法も可能。

#### 【番外編】コンポーネント側をOptions APIで実装する場合

以下のいずれでも動きは上記のComposition APIの時と同じ。

##### storeToRefsを利用する場合

```vue
<script>
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { page, doubleCount } = storeToRefs(counterStore);
		const { increment, pushList } = counterStore;

		return { page, doubleCount, increment, pushList };
	}
};
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ page.count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
			<v-btn @click="pushList()">push list</v-btn>

			<ul>
				<li v-for="title of page.list" :key="title">
					{{ title }}
				</li>
			</ul>
		</v-container>
	</v-main>
</template>
```

※toRefsを利用する場合は、以下のように実装する事もできる。

```vue
<script>
import { toRefs } from 'vue';
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { page, doubleCount } = storeToRefs(counterStore);
		const { increment, pushList } = counterStore;
		const { count, list } = toRefs(page.value);

		return { count, list, doubleCount, increment, pushList };
	}
};
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
			<v-btn @click="pushList()">push list</v-btn>

			<ul>
				<li v-for="title of list" :key="title">
					{{ title }}
				</li>
			</ul>
		</v-container>
	</v-main>
</template>
```

##### storeToRefsを利用しない場合

```vue
<script>
import useCounterStore from '@/stores/counter';

export default {
	setup() {
		const counterStore = useCounterStore();
		const { increment, pushList } = counterStore;

		return { counterStore, increment, pushList };
	}
	computed: {
		count() {
			return this.counterStore.page.count;
		},
		list() {
			return this.counterStore.page.list;
		},
		doubleCount() {
			return this.counterStore.doubleCount;
		}
	}
};
</script>

<template>
	省略
</template>
```

### Options API

PiniaのStoreの実装は以下とする。
```js
import { defineStore } from 'pinia';

export default defineStore('counter', {
	state: () => ({ pageObj: { count: 0, list: ['hoge', 'foo'] } }),
	getters: {
		doubleCount: (state) => state.page.count * 2,
		page: (state) => state.pageObj
	},
	actions: {
		increment() {
			console.log('increment');
			this.page.count += 1;
			console.log(this.page.count);
		},
		pushList() {
			this.page.list.push('test');
		}
	}
});
```

#### reactiveになるパターン

[【番外編】コンポーネント側をOptions APIで実装する場合](#番外編コンポーネント側をoptions-apiで実装する場合-1)と全く同じ。

#### 【番外編】コンポーネント側をComposition APIで実装する場合
##### reactiveになるパターン　use...Store()のstoreをそのまま利用

```vue
<script setup>
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ counterStore.page.count }}</div>
			<div>doubleCount : {{ counterStore.doubleCount }}</div>
			<v-btn @click="counterStore.increment()">counter up</v-btn>
			<v-btn @click="counterStore.pushList()">push list</v-btn>

			<ul>
				<li v-for="title of counterStore.page.list" :key="title">
					{{ title }}
				</li>
			</ul>
		</v-container>
	</v-main>
</template>
```

上記の実装はかなり冗長だが、以下はstoreのリアクティブなオブジェクトをそのまま利用しているので、以下のように期待通りに動作する。
![ezgif.com-video-to-gif (7).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/2457bb47-57f9-440b-58ff-98fc844105f6.gif)

##### reactiveになるパターン　storeToRefsで各プロパティをrefに変換し分割代入を利用できるようにする

```vue
<script setup>
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { page, doubleCount } = storeToRefs(counterStore);
const { increment, pushList } = counterStore;
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ page.count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
			<v-btn @click="pushList()">push list</v-btn>

			<ul>
				<li v-for="title of page.list" :key="title">
					{{ title }}
				</li>
			</ul>
		</v-container>
	</v-main>
</template>
```

上記のように実装しても[reactiveになるパターン　use...Store()のstoreをそのまま利用](#reactiveになるパターンusestoreのstoreをそのまま利用-2)と同じように期待通りに動作する。もう少し工夫するとすれば、[ref() と共に使うリアクティブな変数](https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-variables-with-ref)に以下のように書かれている通り、ref()でオブジェクト型を保持する場合の`value`プロパティの値は、リアクティブなオブジェクト（reactive()）になる。

> また、オブジェクト型を保持する場合、ref は .value を reactive() で自動的に変換します。

そのため、[toRefs()](https://ja.vuejs.org/api/reactivity-utilities.html#torefs)を利用して、reactive()で生成されたオブジェクト（リアクティブなオブジェクト）の各プロパティをrefに変換し、分割代入を可能にする事もできる。具体的な実装は以下。
```vue
<script setup>
import { toRefs } from 'vue';
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { page, doubleCount } = storeToRefs(counterStore);
const { count, list } = toRefs(page.value);
const { increment, pushList } = counterStore;
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
			<v-btn @click="pushList()">push list</v-btn>

			<ul>
				<li v-for="title of list" :key="title">
					{{ title }}
				</li>
			</ul>
		</v-container>
	</v-main>
</template>
```

##### reactiveになるパターン②　[toRefs()](https://ja.vuejs.org/api/reactivity-utilities.html#torefs)を利用してreactive()をプレーンオブジェクトに変換に変換

[公式](https://ja.vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-variables-with-ref)に以下のように書かれている通り、ref()にオブジェクトが渡されたとき、`.value`プロパティはreactive()なオブジェクトになる。

> また、オブジェクト型を保持する場合、ref は .value を reactive() で自動的に変換します

そのため、[toRefs()](https://ja.vuejs.org/api/reactivity-utilities.html#torefs)を利用して、reactive()なオブジェクトの各プロパティを元のオブジェクトの対応するプロパティを指すref()に変換する事ができ、今回で言うと`page`はreactive()なオブジェクトなので実装としては以下のようにする事もできる。

```vue
<script setup>
import { toRefs } from 'vue';
import { storeToRefs } from 'pinia';
import useCounterStore from '@/stores/counter';

const counterStore = useCounterStore();
const { page, doubleCount } = storeToRefs(counterStore);
const { increment, pushList } = counterStore;

const { count, list } = toRefs(page.value);
</script>

<template>
	<v-main>
		<v-container>
			<div>count : {{ count }}</div>
			<div>doubleCount : {{ doubleCount }}</div>
			<v-btn @click="increment()">counter up</v-btn>
			<v-btn @click="pushList()">push list</v-btn>

			<ul>
				<li v-for="title of list" :key="title">
					{{ title }}
				</li>
			</ul>
		</v-container>
	</v-main>
</template>
```