Vue3でオブジェクトをrefでラップしたら内部のリアクティブ値の型が変わってハマった
問題
こんな感じの事をするとcomputedの型が消えてしまいハマりました
const three = ref(3)
const object = {
one: 1,
two: 2,
three: computed(() => {
return three.value
}),
}
const objectRef = ref(object)
objectRef.value = object // ここで型エラー
作ろうとした物
YouTube動画とローカル動画の切り替えと再生・停止が可能な機能の実装をしました。
このような構造で、YouTubeとローカル動画の操作をインターフェイスで共通化し、利用側のApp.vueでリアクティブなクラスを切り替える形で実装しました。
├── App.vue // エントリーポイント
├── IVideoState.ts // 動画状態のinterface
├── LocalVideoState.ts // ローカル動画状態
├── YoutubeVideoState.ts// Youtube動画状態
コード
import { ComputedRef } from "vue"
export interface IVideoState {
play(): void
stop(): void
subscription: {
status: ComputedRef<VideoStatus>
videoType: ComputedRef<VideoType>
}
}
export enum VideoStatus {
NOT_CREATED = "未作成",
CREATED = "作成済",
PLAYING = "再生中",
STOPED = "停止中",
}
export enum VideoType {
YOUTUBE = "YOUTUBE",
LOCAL = "LOCAL",
}
LocalVideoState.ts
import { InjectionKey, ref, computed } from "vue"
import { IVideoState, VideoStatus, VideoType } from "./IVideoState"
// Youtubeの状態クラス
class UseYoutubeVideoState implements IVideoState {
private status = ref(VideoStatus.NOT_CREATED)
constructor() {
// ここでYoutube動画のプレイヤーを作成する
this.status.value = VideoStatus.CREATED
}
play = () => {
this.status.value = VideoStatus.PLAYING
}
stop = () => {
this.status.value = VideoStatus.STOPED
}
subscription = {
status: computed(() => {
return this.status.value
}),
videoType: computed(() => {
return VideoType.YOUTUBE
}),
}
}
const YoutubeVideoStateKey: InjectionKey<IVideoState> = Symbol("YoutubeVideoState")
export { UseYoutubeVideoState, YoutubeVideoStateKey }
YoutubeVideoState.ts
import { InjectionKey, ref, computed } from "vue"
import { IVideoState, VideoStatus, VideoType } from "./IVideoState"
// ローカル動画
class UseLocalVideoState implements IVideoState {
private status = ref(VideoStatus.NOT_CREATED)
constructor() {
// ここでローカル動画のプレイヤーを作成する
this.status.value = VideoStatus.CREATED
}
play = () => {
this.status.value = VideoStatus.PLAYING
}
stop = () => {
this.status.value = VideoStatus.STOPED
}
subscription = {
status: computed(() => {
return this.status.value
}),
videoType: computed(() => {
return VideoType.LOCAL
}),
}
}
const LocalVideoStateKey: InjectionKey<IVideoState> = Symbol("LocalVideoState")
export { UseLocalVideoState, LocalVideoStateKey }
<script setup lang="ts">
import { UseYoutubeVideoState } from "./YoutubeVideoState"
import { UseLocalVideoState } from "./LocalVideoState"
import { ref } from "vue"
const video = ref(new UseYoutubeVideoState())
const hundlePlayClick = () => {
video.value.play()
}
const hundleStopClick = () => {
video.value.stop()
}
const hundleVideoChange = () => {
// >>>> ★★★ここで怒られる★★★ <<<<
video.value = new UseLocalVideoState()
}
</script>
<template>
<div style="width: 300px; background: gainsboro">
<div
style="
width: 100%;
height: 150px;
background: gray;
text-align: center;
color: white;
"
>
{{ video.subscription.videoType.value }}
</div>
<p>status: {{ video.subscription.status.value }}</p>
<button @click="hundlePlayClick()">再生</button>
<button @click="hundleStopClick()">停止</button><br />
<button @click="hundleVideoChange()">動画を変更</button>
</div>
</template>
refでラップしたクラスを同じインターフェイスを持つ別のクラスで置き換えようとした場合に、型エラーが発生しました。
subscripitonオブジェクトのプロパティが、なぜか算出プロパティではなくなってしまいました。
なぜ型が変わったのか?
公式にこんな一文があります。
If an object is assigned as a ref's value, the object is made deeply reactive with reactive(). This also means if the object contains nested refs, they will be deeply unwrapped.
DeepLで和訳するとこんな感じ。
オブジェクトがrefの値として割り当てられた場合、そのオブジェクトはreactive()でディープリアクティブにされます。これは、オブジェクトがネストしたrefを含んでいる場合、それらが深くアンラップされることも意味します。
refでオブジェクトをラップすると、オブジェクト内に含まれるrefが解除されてしまい、オブジェクト全体がリアクティブ化されてしまうらしい。
全体をリアクティブ化せずにvideo.valueのみをリアクティブ化するには、shallowRefを使えとの事。
なのでshallowRefを使用して以下のように修正します。
import { UseLocalVideoState } from "./LocalVideoState"
import { ref } from "vue"
- const video = ref(new UseYoutubeVideoState())
+ const video = shallowRef<IVideoState>(new UseYoutubeVideoState())
これでOK。
おわりに
雰囲気でrefを使っていたせいでハマってしまいました・・・・・
公式ドキュメントはちゃんと読みましょう・・・・
Discussion