12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vue3.5】リリースされた機能まとめ

Last updated at Posted at 2024-10-18

概要

1ヶ月ほど前ですが、Vue.jsの3.5がリリースされましたので、各機能について大まかにまとめました。

現在は様々なバグの修正などが行われ 3.5.12までリリースされています(2024/10/15現在)

Vue 3.5の新機能と改善点について

Vue 3.5では、多くの新機能とパフォーマンスの最適化が導入されました。主な改善点や新機能を私なりに噛み砕いてまとめていきます。

公式の文章は本記事の先頭に添付してありますので、ご確認ください。

1. リアクティブシステムの最適化

メモリ使用量が56%削減

リアクティブシステムが大幅にリファクタリングされ、パフォーマンスが向上し、メモリ使用量が56%削減されました。この改善により、SSR(サーバーサイドレンダリング)中に発生していた、不要な計算値やメモリリークの問題が解消されました。

リアクティブ配列に対する操作高速化

大規模で深いリアクティブ配列に対する操作が最大10倍高速化されました。

2. リアクティブなpropsの分割代入

リアクティブなpropsの分割代入が標準機能として安定化されました。以前は分割代入を行うとリアクティブ性が失われてしまうという問題もありましたが、修正後は<script setup>内でdefinePropsから分割代入された変数は、リアクティブになります。この機能により、JavaScriptの標準的なデフォルト値の構文を利用して、propsの宣言を大幅に簡素化できます。

Vue3.3よりpropsDestructureをtureで設定すれば、分割代入も可能になりましたが3.5からは宣言を行わずとも標準で使用できるようになりました。

Vue3.5になっていても、最新(2024/10/15現在)のNuxt.js v3.13上で動作させる場合は依然として設定が必要です。

変更前:

const props = withDefaults(
  defineProps<{
    count?: number
    msg?: string
  }>(),
  {
    count: 0,
    msg: 'hello'
  }
)

変更後:

const { count = 0, msg = 'hello' } = defineProps<{
  count?: number
  msg?: string
}>()

分割代入された変数(例えば、count)にアクセスすると、その変数はコンパイラによって自動的にprops.countとして処理され、アクセス時にトラッキングされます。

SSR(サーバーサイドレンダリング)の改善

SSRに関するいくつかの改善が行われました。特に、非同期コンポーネントがいつハイドレーションされるべきかを制御できるようになり、defineAsyncComponent APIのhydrateオプションでハイドレーションの方針を指定できるようになりました。これにより、コンポーネントが可視化されたときにのみハイドレーションされるように設定できます。

・ハイドレーションとは
SSRによって生成された静的なHTMLに対して、フロントエンドフレームワークがクライアントサイドでJavaScriptを結びつけ、インタラクティブな機能を追加する処理です。

import { defineAsyncComponent, hydrateOnVisible } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Comp.vue'),
  hydrate: hydrateOnVisible()
})

新しいAPI: useId()

useId()は、アプリケーションごとに一意で、サーバーとクライアント間で一貫性のあるIDを生成するための新しいAPIです。このAPIを使用することで、フォーム要素やアクセシビリティ属性のIDを生成し、SSRアプリケーションでハイドレーションミスマッチを防ぐことができます。

Reactで実装されているuseIdと同じものと認識しています。

prefixを明示的に設定することも可能です。

app.config.idPrefix = 'my-app-'
<script setup>
import { useId } from 'vue'

const id = useId() // 出力例 )my-app-v-0
</script>

<template>
  <form>
    <label :for="id">Name:</label>
    <input :id="id" type="text" />
  </form>
</template>

Math.random()やDate.now()などを使用して一意なIDを生成しようとすると、サーバーサイドとクライアントサイドで異なるIDが生成されてしまい、ハイドレーションミスマッチが起きる原因になってしまうのでとても便利だと思います。

・ハイドレーションミスマッチとは
サーバーサイド側で生成されたHTMLと、クライアントサイド側で生成されたHTMLが一緒だよねというのを確認する際に不整合が起きてしまうこと

カスタム要素の改善

defineCustomElement APIに関連する多くの長年の問題が修正され、カスタム要素の作成に関する新機能が追加されました。

Web Componentsに対して細かい設定が出来るようになったよという感じです。

Web Componentsは、<my-component>のような形で、自分で作成できるカスタムHTML要素です。

例えば、シャドウDOMという、カプセル化したコンポーネントに対して外部からの影響を受けないようにする全体のDOMとは別のWeb Components用のDOMがあるのですが、そのシャドウDOMを無効化して全体のCSSやJSの影響を受けるようにする設定がshadowRoot: falseになります。

その他にも様々な改善があったのでWeb Componentsを利用する場合は本記事の先頭に載せた「Announcing Vue3.5」の中で書かれていますので、他にも随時最新の情報をキャッチアップしていきましょう。

import MyElement from './MyElement.ce.vue'

defineCustomElements(MyElement, {
  shadowRoot: false,
  nonce: 'xxx',
  configureApp(app) {
    app.config.errorHandler = ...
  }
})

useTemplateRef()の導入

Vue 3.5では、新しい方法でTemplate Refを取得できるuseTemplateRef() APIが導入されました。このAPIを使用することで、動的なIDにバインドされた参照をサポートできます。

つまり前のrefではtemplateタグ内に直接書かれていない動的に作成される要素にrefを使用できなかったのが、今回から出来るようになりました。

順を追って説明していきます。

1. 従来の ref の使用方法

Vue 3.5 以前のバージョンでは、テンプレート内の要素にアクセスするために、従来のrefを使用します。以下のようにテンプレート内で ref 属性を指定し、対応するコンポーネントのスクリプト部分で変数名を一致させることで、DOM 要素への参照を取得できました。

下記コード、ドキュメントより引用

<script setup>
import { ref, onMounted } from 'vue'

// 要素の参照を保持する ref を宣言します。
// 名前は、テンプレートの ref の値に一致させる必要があります。
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

この方法では、公式のコメントアウトでもあるようにテンプレート内の ref 属性名と、スクリプト内の変数名が一致している必要がありました。この「静的な ref 属性」がコンパイラによって解析可能である必要があるため、動的に ref を変更することが難しいという制約がありました。

2. Vue 3.5の新しい useTemplateRef() API

Vue 3.5 では、上記の制約を解消するために useTemplateRef() API が導入されました。このAPIでは、静的な ref 属性に依存せず、ランタイムで文字列IDを使ってテンプレート要素にアクセスできるようになっています。

以下が新しい useTemplateRef() の使用例です。

<script setup>
import { useTemplateRef } from 'vue'

const inputRef = useTemplateRef('input')
</script>

<template>
  <input ref="input">
</template>

このコードでは、useTemplateRef('input') を使って、テンプレート内の ref="input" がつけられた 要素を取得しています。この方法のメリットは、テンプレート内の ref 属性が「静的」である必要がない点です。ランタイムでIDに基づいて ref を解決するため、動的にIDを変更する場合でも、正しく対応できます。

そのコード静的でわかりにくくないか?とアナウンスの記事を見ても思ったのでメリットと具体的な動的な例をしまします

3. useTemplateRef() のメリット

動的な ref への対応

※具体的なコードは見る必要ありません。ざっとrefが動的に付与されている箇所を見てください。

<!-- 追加されたタスクにrefを適用するTODOアプリ -->
<script setup>
import { ref, onUpdated, useId } from 'vue'

// タスクデータ
const tasks = ref([])
const newTask = ref('')
let taskRefs = ref([])

// タスクを追加
function addTask() {
  if (newTask.value.trim() !== '') {
    tasks.value.push({
      id: useId(),
      name: newTask.value
    })
    newTask.value = ''
  }
}

// タスクを削除
function removeTask(index) {
  tasks.value.splice(index, 1)
}

// タスクの動的なrefにアクセス
onUpdated(() => {
  taskRefs.value = tasks.value.map((task, index) => refs[`task-${index}`])
  console.log(taskRefs.value)  // 各タスクのDOM要素にアクセス
})
</script>

<template>
  <div>
    <input v-model="newTask" placeholder="タスクを記入してください">
    <button @click="addTask">追加</button>

    <ul>
      <li v-for="(task, index) in tasks" :key="task.id" :ref="`task-${index}`">
        {{ task.name }}
        <button @click="removeTask(index)">削除</button>
      </li>
    </ul>
  </div>
</template>

この例では、refs[task-${index}] の値が変わるたびに、useTemplateRef() が新しい参照を正しく取得するようになります。

Deferred Teleport

<Teleport>コンポーネントでは、テレポート先がないとコンテンツがうまく表示されないという問題がありましたが、3.5からは defer プロパティを使うことで、テレポート先がレンダリングされた後にコンテンツをマウントできるようになりました。

従来の動作:
<Teleport>コンポーネントがマウントされる際、指定したターゲット要素がDOM内に存在しないと、テレポートする内容が適切にレンダリングされませんでした。このため、ターゲット要素が後から追加された場合などには、意図した通りに表示されないことがありました。

改善された動作:
Vue 3.5からの、defer プロパティを使うことで、従来の問題が解消されました。defer プロパティがあることで、<Teleport>コンポーネントは現在のレンダリングサイクルの後にマウントされるため、テレポート先が存在しない場合でも問題なくコンテンツを表示できるようになりました。

例えば、<Teleport>コンポーネントの使用例としてモーダルコンポーネント内でローディング画面などを表示する際、画面全体を覆うコンポーネントを body タグにテレポートすることで、表示位置のズレを解消することができます。これにより、その為の処理を追加したり、モーダルを配置している親コンポーネントまでフラグをemitバケツリレーしなくともよくなります。

<Teleport defer target="#container">...</Teleport>
<div id="container"></div>

上記の動作も defer プロパティをつけることで、ターゲット要素(#container)が後から DOM に追加された場合でも、Teleport のコンテンツは正しく配置されます。後方互換性を保つ為にVue3.5時点ではデフォルトでの実装はされていません。

onWatcherCleanup()の導入

Vue 3.5では、onWatcherCleanup()というAPIが導入され、ウォッチャー内でクリーンアップコールバックを登録できるようになりました。これにより、古いリクエストの中断などの処理が可能になります。

例えばカウントの変更を検知するウォッチャーの場合、ユーザーがボタンを連打してカウントが急激に増加する場合、各クリックに対してAPIリクエストを行うと、複数のリクエストが同時に実行される可能性があります。その時に古いリクエストの結果が画面に表示されてしまうと、ユーザー側の混乱を招く結果になりかねません。このため、古いリクエストをキャンセルして、常に最新のリクエストだけを扱うようにすることができるようになります。

なので、最新ではないデータをキャンセルすることで、アプリケーションのパフォーマンスやユーザーエクスペリエンスを向上させることにつながります。

<script setup>
import { ref, watch, onWatcherCleanup } from 'vue'

const count = ref(0)
const result = ref(null)

const incrementCount = () => {
  count.value++
}

watch(count, (newCount) => {
  const controller = new AbortController()
  const signal = controller.signal

  fetchData(newCount, signal)
    .then(data => {
      if (!signal.aborted) {
        result.value = data
      }
    })
    .catch(error => {
      if (error.name !== 'AbortError') {
        console.error('Fetch error:', error)
      }
    })

  onWatcherCleanup(() => {
    controller.abort()
  })
})

async function fetchData(count, signal) {
  const response = await fetch(`https://api.example.com/data/${count}`, { signal })
  if (!response.ok) {
    throw new Error('Network response was not ok')
  }
  return await response.json()
}
</script>

<template>
  <div>
    <button @click="incrementCount">Count: {{ count }}</button>
    <p>Latest result: {{ result }}</p>
  </div>
</template>

まとめ

Vue 3.5のアップデートは、パフォーマンスの向上などもありましたが、開発者体験を改善する内容が結構あったなと感じました。新機能などもいくつか追加されましたが、実際リリースされてから、個人的に使用頻度も結構あります。

常に最新の情報を追いかけて、業務に活かしていくのはとても大事だなと感じました。

12
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?