クラウドサインのフロントエンドエンジニアの辻@t0daaayです。
2024 年 9 月 1 日に Vue 3.5 のリリースが発表されました。
https://blog.vuejs.org/posts/vue-3-5
このブログでは、このリリースノートを読みリリースされた内容を実際に動かしてみたり、さらに調査した内容についてまとめました。
Reactive Props Destructure の安定化
https://vuejs.org/guide/extras/reactivity-transform.html#reactive-props-destructure
props
が分割代入の形式でもリアクティブを保てるようになりました。
書き方がかなり簡潔になっています。
const { count = 0, msg = 'hello' } = defineProps<{ count?: number; msg?: string; }>();
動作確認用: StackBlitz
今までの書き方
withDefaults
を使う必要があり、新しい書き方より冗長になっていました。
const props = withDefaults( defineProps<{ count?: number msg?: string }>(), { count: 0, msg: 'hello' } )
useTemplateRef()
https://vuejs.org/guide/essentials/template-refs.html
Template Ref の取得が以前よりも分かりやすく書けるようになりました。
今までのようにテンプレートの ref
属性の値と一致する名前を持つ ref
を宣言する必要がなくなっています。
また型推論が適用されるようになり、より型安全になっています。
<script setup lang="ts"> import { useTemplateRef } from 'vue'; const dialogRef = useTemplateRef('myDialog'); const openDialog = () => { dialogRef.value.showModal(); }; </script> <template> <button @click="openDialog">ダイアログを開く</button> <dialog ref="myDialog"> <form method="dialog"> ダイアログ <button type="submit">閉じる</button> </form> </dialog> </template>
動作確認: StackBlitz
今までの書き方
前述のとおり、テンプレートの ref
属性の値と一致する名前を持つ ref
を宣言する必要がありました。
また myDialog
が何に使われるのかが変数だけ見ても分からないため、新しい記法に比べて可読性が下がります。
<script setup lang="ts"> import { ref } from 'vue'; const myDialog = ref<null | HTMLDialogElement>(null); const openDialog = () => { myDialog.value?.showModal(); }; </script> <template> <button @click="openDialog">ダイアログを開く</button> <dialog ref="myDialog"> <form method="dialog"> ダイアログ <button type="submit">閉じる</button> </form> </dialog> </template>
Deferred Teleport
https://ja.vuejs.org/guide/built-ins/teleport#deferred-teleport
defer
プロパティを使い、Teleport
を後のレンダーサイクルでマウント可能になっています。
※ターゲット要素はテレポートと同じマウント / 更新ティックでレンダリングされる必要があります。
<script setup lang="ts"> import { ref } from 'vue'; const isVisible = ref(false); </script> <template> <button @click="isVisible = true">テレポート先を表示</button> <template v-if="isVisible"> <Teleport defer to="#late-div"> <div>defer が設定されるとテレポートが成功する</div> </Teleport> <!-- defer がない場合、 Teleport の id 参照タイミングでレンダリングされていない --> <div id="late-div"></div> </template> </template>
動作確認: StackBlitz
onWatcherCleanup()
https://vuejs.org/api/reactivity-core#onwatchercleanup
現在のウォッチャーが再実行される直前に実行されるクリーンアップ関数を登録する API が登場しました。
以下のコードでは、timeLeft
の値が変更されるたびに新しいカウントダウンタイマーが作成されますが、onWatcherCleanup
内で以前のタイマーをクリアするようにしています。
<script setup lang="ts"> import { ref, watch, onWatcherCleanup } from 'vue'; const timeLeft = ref(10); const countdown = ref(10); watch(timeLeft, (newTime) => { countdown.value = newTime; const intervalId = setInterval(() => { if (countdown.value > 0) { countdown.value--; } else { clearInterval(intervalId); } }, 1000); // 現在のウォッチャーが再実行される直前に実行される onWatcherCleanup(() => { console.log('onWatcherCleanup'); clearInterval(intervalId); }); }); </script> <template> <div> <p>秒数を変更するとカウントダウン開始</p> <input type="number" v-model="timeLeft" placeholder="Set timer" /> <p>残り時間: {{ countdown }}</p> </div> </template>
動作確認: StackBlitz
今までの書き方
第三引数のonCleanup
を呼び出す必要があり、新しい API を使った記法より分かりづらく、直感性に欠けていました。
<script setup lang="ts"> import { ref, watch } from 'vue'; const timeLeft = ref(10); const countdown = ref(10); watch(timeLeft, (newTime, _, onCleanup) => { countdown.value = newTime; const intervalId = setInterval(() => { if (countdown.value > 0) { countdown.value--; } else { clearInterval(intervalId); } }, 1000); // 現在のウォッチャーが再実行される直前に実行されるクリーンアップ関数を登録する。 onCleanup(() => { clearInterval(intervalId); }); }); </script> <template> <div> <p>秒数を変更するとカウントダウン開始</p> <input type="number" v-model="timeLeft" placeholder="Set timer" /> <p>残り時間: {{ countdown }}</p> </div> </template>
SSR の改善
ハイドレーションに関連するアップデートがいくつかあります。
遅延ハイドレーション
https://ja.vuejs.org/guide/components/async.html#lazy-hydration
非同期コンポーネントがいつハイドレーションされるかを制御できるようになりました。
defineAsyncComponent()
API の hydrate
オプションを使用し、ハイドレーションのタイミングを設定できるようになっています。
import { defineAsyncComponent, hydrateOnVisible } from 'vue' const AsyncComp = defineAsyncComponent({ loader: () => import('./Comp.vue'), // 表示時にハイドレーションする場合 hydrate: hydrateOnVisible() })
useId()
https://ja.vuejs.org/api/composition-api-helpers.html#useid
アクセシビリティ属性やフォーム要素に対して、アプリケーションごとに一意な ID を生成するための API が提供されるようになりました。
<script setup lang="ts"> import { useId } from 'vue' const id = useId() </script> <template> <form> <label :for="id">Name:</label> <input :id="id" type="text" /> </form> </template>
data-allow-mismatch
https://ja.vuejs.org/api/ssr.html#data-allow-mismatch
クライアントの値がサーバーの値と必然的に異なる場合(例: 日付)、data-allow-mismatch
属性を使用することで、ハイドレーションミスマッチの警告を抑制できるようになりました。
<span data-allow-mismatch>{{ data.toLocaleString() }}</span>
defineCustomElements の改善
ここは前提となる知見がなかったため、順を追って調べました。
defineCustomElements
について。
https://ja.vuejs.org/guide/extras/web-components#definecustomelement
このメソッドは defineComponent と同じ引数を受け取りますが、代わりにネイティブのカスタム要素クラスのコンストラクタを返します。
カスタム要素について。
https://ja.vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue
カスタム要素の最大の利点は、どんなフレームワークでも、あるいはフレームワークがなくても使用できるということです。そのため、利用者が同じフロントエンドスタックを使用していない場合にコンポーネントを配布する場合や、アプリケーションが使用するコンポーネントの実装の詳細から該当アプリケーションを隔離したい場合に最適です。
つまり、 defineCustomElements
は Vue に依存しないウェブコンポーネントの作成に役立つ API ということです。
今回のアップデートでは、その defineCustomElements
の多くの問題の修正と、Vue でカスタム要素を作成するための多くの新機能が追加されています。
リアクティブシステムの最適化
リファクタリングにより、動作の変更なしにパフォーマンスの向上がされたとのことです。
感想
個人的には、props
の書き方が簡潔になったことと、useTemplateRef
による可読性の向上と型推論の適用がかなり嬉しいです。
業務でも積極的に今回のアップデート内容を取り入れて、積極的に改善を進めていきたいです。