💥

Vue 3.3 + Vite で defineProps の分割代入や watch がうまく動作しないときは

2023/08/15に公開

結論

vite.config.tsplugins にある vue のオプションに propsDestructure: true を追加してください。これによって下記に挙げる問題は解消され、期待した動作になります。

vite.config.ts
export default defineConfig({
  plugins: [
    vue({
      script: {
        propsDestructure: true,
      },
    }),
  ],
});

動作確認バージョン

  • Vue: 3.3.4
  • Vite: 4.4.9
  • TypeScript: 5.1.6

背景

Vue 3.3 から defineProps で受け取ったプロパティを分割代入できるようになりました。props を宣言する必要がなくなり、リアクティビティを維持しつつもプロパティを直接指定することができるようになりました。

PropsTest.vue (Vue 3.2 以前)
<script setup lang="ts">
const props = defineProps<{
  text: string;
  important?: boolean;
}>();
</script>

<template>
  <div :style="`color: ${props.important ? 'red' : 'black'}`">{{ props.text }}</div>
</template>
PropsTest.vue (Vue 3.3 以降)
<script setup lang="ts">
const { text, important } = defineProps<{
  text: string;
  important?: boolean;
}>();
</script>

<template>
  <div :style="`color: ${important ? 'red' : 'black'}`">{{ text }}</div>
</template>

発生した問題

下記で発生した問題は冒頭の結論で示した、vite.config.tspropsDestructure: true を追加 しない ことで発生するものです。現段階でこれらの問題は警告が出されず、ビルドも成功してしまいます。いずれの問題も実行時に発覚するもので、vite.config.ts の内容を再確認すべきです。

デフォルト値に true が代入できない

デフォルト値も分割代入部分に記述できるようになり、withDefaults を使う必要が無くなりました。

しかしデフォルト値に true を指定しても実際は false が代入されてしまいます。もちろん親コンポーネントから :important="true" のように指定すれば代入ができますし、Boolean 型以外の型であれば正しく動作します。

PropsTest.vue
<script setup lang="ts">
const { text, important = true } = defineProps<{
  text: string;
  important?: boolean;
}>();

console.info(important);  // 親コンポーネントで未指定であると false が出力される
</script>

withDefaults を使って対応はできる

vite.config.tspropsDestructure: true を指定しなくても、withDefaults を使ってデフォルト値を正しく指定することはできます。

しかし、withDefaults でできることは分割代入のデフォルト値指定で達成でき、さらに withDefaults を deprecated とするPRが提出され(ただし revert されています)、Vue 3.4 以降のどこかで削除されることは確実と思われます。

PropsTest.vue
<script setup lang="ts">
const { text, important } = withDefaults(defineProps<{
    text: string;
    important?: boolean;
  }>(),
  { important: true });
</script>

watch で変更が検知できない

分割代入をすると watch (または watchEffect)を使った変更が検知できなくなります。

PropsTest.vue
<script setup lang="ts">
const { text, important } = defineProps<{
  text: string;
  important?: boolean;
}>();

watch(() => text, () => { console.info(`text changed: ${text}`); });
</script>

なお、たとえ propsDestructure: true を指定しても watch の第1引数にプロパティを直接指定することはできません。公式ガイドのとおり、propsの値を指定する場合は getter を使った方法にしなくてはなりません。これもまた警告がなく、見落としやすいポイントになります。

PropsTest.vue
watch(text, () => { console.info(`text changed: ${text}`); });        // NG
watch(() => text, () => { console.info(`text changed: ${text}`); });  // OK

追記

Vue 3.3.4 現在、以下のような警告が表示されます。存続か廃止か、未だ議論ある機能ですので当該のRFCを確認しておくとよいかもしれません。

[@vue/compiler-sfc] This project is using reactive props destructure, which is an experimental  feature. It may receive breaking changes or be removed in the future, so use at your own risk.
To stay updated, follow the RFC at https://github.com/vuejs/rfcs/discussions/502.

参考文献

Discussion