【Vue3】俺のcomputed(() => x.value)の値が子コンポーネントに勝手にアップデートされるんだが【TypeScript】
追記: 🙅♂️->「`core.value`が子コンポーネントに更新される」 | 🙆♂️->「`core.value.value`が子コンポーネントに更新される」
追記概要
この記事は
- 「
core.valueが子コンポーネントに更新される」
ではなく - 「
core.value.valueが子コンポーネントに更新される」
を意図して書いています!
誤解の防止のために、追記させていただきました🙌
おそらく上述の説明のみで十分かと思いますが、以下に詳細を記します。
通常はこの折り畳みを閉じてもらい、続きを読んでください。
もし後述の本編を読み、上述の追記の意味がわからなければ、説明の順序が前後しますが、下記の追記詳細を読んでください。
追記詳細
core.value.valueとcore.value(= proxy.value)がまぎらわしく、誤解させることに気が付きました 🙏
意図としては後述では「core.valueが子コンポーネントに更新される」ということを言いたいわけではなく、「core.value.valueが子コンポーネントに更新される」ということです。
具体的には、以下のようなB.vueのupdateFooBarにより、A.vueのfooComputedごしに、foo.value.barが更新されるということです。
(追記なので、以下は実働を確認しておりません。失礼します!)
<template>
<p>foo.value is {{ foo.value }}</p>
<!-- foo.value is { bar: 42 } --> <!-- クリックをしていない状態 -->
<!-- foo.value is { bar: 52 } --> <!-- クリック1回目 -->
<!-- foo.value is { bar: 62 } --> <!-- クリック1回目 -->
<!-- ... -->
<B :foo="fooComputed" />
</template>
<script setup lang="ts">
const foo = ref({ bar: 42 })
const fooComputed = computed(() => foo.value)
</script>
<template>
<button @click="updateFooBar">update</button>
</template>
<script setup lang="ts">
const { foo } = defineProps<{
foo: { bar: number }
}>()
function updateFooBar() {
foo.bar += 10
}
</script>
ここでの.vueと、後述の.vueの概念の対応としては、以下のようになります。
- A.vue <-> HelloWorld.vue
- B.vue <-> Child.vue
俺のcomputed(() => x.value)の値が子コンポーネントに勝手にアップデートされるんだが
-
type TがT extends Record<string, unknown>で - かつ
computed({ get, set })でなく、computed(() => x.value)の形だった(WritableComputedRefでなかった)
な場合の話。
(余談だけど、T extends Objectみたいに、Object型は使ってないですよね?)
結論

ComputedRef(computedな値)のproxyと、Ref(computedの元になる値)のcoreを定義する側:
<template>
<p>core.value is {{ core.value }}</p>
<Child :proxy />
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import Child from './Child.vue'
const core = ref({ value: 42 })
const proxy = computed(() => core.value)
// オブジェクトの受け渡しはシャローコピーなので、 core.value.valueは `<Child>` 経由で更新できる。
// ただしそのとき、core.valueが更新されたわけではないので、この`watch()`は実行されない。
watch(core, () => console.log('poi: core is updated'))
</script>
ComputedRefを更新する側(こちらはproxyをpropsとして(単なる値として)しか認識していないことに注意):
<template>
<button @click="updateProxyValue">update</button>
</template>
<script setup lang="ts">
const { proxy } = defineProps<{
proxy: { value: number };
}>();
function updateProxyValue() {
proxy.value += 10;
}
</script>
これを防ぐ方法。
<template>
<p>core.value is {{ core.value }}</p>
<DeepReadonlyChild :proxy />
</template>
<script setup lang="ts">
import { ref, computed, readonly } from 'vue'
import DeepReadonlyChild from './DeepReadonlyChild.vue'
const core = ref({ value: 42 })
const proxy = computed(() => readonly(core.value)) // readonlyを追加
</script>
<template>
<button @click="updateProxyValue">update</button>
</template>
<script setup lang="ts">
import type { DeepReadonly } from "vue"
const { proxy } = defineProps<{
proxy: DeepReadonly<{ value: number }> // DeepReadonlyで期待
}>()
function updateProxyValue() {
proxy.value += 10 // typecheck error !
}
</script>
用語の乱用
この記事では以降、わかりやすさを重視するため、慣習・口語での説明にならい、以下の用語の乱用を行います。
-
computed:const x = computed(/* ... */)のような変数x、もしくはcomputed関数そのもの -
ref:const x = ref(/* ... */)のような変数x、もしくはref関数そのもの -
props: 子コンポーネントのconst props = defineProps<{ /* ... */ }>()のような変数props
computedを子コンポーネントのpropsに渡しても、変更されないのか?
変更されます。
よく考えたら当たり前で
<Child :proxy />
const proxy = computed(() => core.value)
のような渡し方は、関数でいうシャローコピーの渡しになるからです。
(ソースを読んだ感想です)
const { proxy } = defineProps<{
proxy: { value: number }
}>()
function updateProxyValue() {
proxy.value += 10
}
ここでproxyはシャローコピーされた、const proxy = computed(() => core.value)とは別のオブジェクトですが、proxy.value(後者で言うproxy.value.value)は同じ参照先です。
ですので、updateProxyValueはconst core = ref({ value: 42 })のcore.value.valueを更新するというわけですね。
結論
オブジェクト型のcomputedのプロパティは、子コンポーネントで更新され得ます。
それを防ぐためには、vueのreadonly(DeepReadonly)を使いましょう。
<template>
<p>core.value is {{ core.value }}</p>
<DeepReadonlyChild :proxy />
</template>
<script setup lang="ts">
import { ref, computed, readonly } from 'vue'
import DeepReadonlyChild from './DeepReadonlyChild.vue'
const core = ref({ value: 42 })
const proxy = computed(() => readonly(core.value)) // readonlyを追加
</script>
<template>
<button @click="updateProxyValue">update</button>
</template>
<script setup lang="ts">
import type { DeepReadonly } from "vue"
const { proxy } = defineProps<{
proxy: DeepReadonly<{ value: number }> // DeepReadonlyで期待
}>()
function updateProxyValue() {
proxy.value += 10 // typecheck error !
}
</script>
終わり!
Discussion