弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Vue 3.5 の新機能・改善点まとめ

クラウドサインのフロントエンドエンジニアの辻@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 による可読性の向上と型推論の適用がかなり嬉しいです。

業務でも積極的に今回のアップデート内容を取り入れて、積極的に改善を進めていきたいです。