Vue 3.4 以降の新しい機能:`defineModel`、リアクティブなデストラクチャリング、および `useTemplateRef` を詳しく解説
Vue 3.4+(2023年12月リリース)では、いくつかの画期的な改善が導入されました。具体的には、defineModel マクロの簡素化 v-model、リアクティブなプロパティの展開 useTemplateRef、Composition API、そして useId 一意のID生成機能です。これらの機能により、テンプレートコードが大幅に簡素化され、開発体験が向上します。
このレッスンでは、Vue 3 のコードをより簡潔でモダンなものにする 5 つの重要な新機能を習得できます。
1. 学習内容
defineModelv-modelの複雑な構文を置き換えるマクロ- レスポンシブプロパティの分解(デフォルト:ディープレスポンス)
useIdSSR対策済みの一意のIDを生成するuseTemplateRefモジュール型リファレンス- 遷移アニメーションの改善
- Vue 3.4 対 Vue 3.3 対 Vue 3.2:主な違い
- おすすめのアップグレード方法 5選
2. v-model コンポーネントにおける「親子同期」の課題
(1) 課題:propsが5行+emitが5行=計10行の定型コード
Aliceは、v-model を使用してカスタム入力コンポーネントを作成しました:
JS
// ❌ The "Flip" Version:v-model Required 10 Sample Code
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const internalValue = ref(props.modelValue)
watch(() => props.modelValue, (val) => {
internalValue.value = val
})
watch(internalValue, (val) => {
emit('update:modelValue', val)
})
10行の定型コード――v-modelコンポーネントごとにこれを記述する必要があります。
主任開発者:
「アリス、これは冗長すぎるよ。Vue 3.4には、これを1行で実現できる
defineModelがあるんだ。」
(2) 図3.4:defineModelマクロの一般解
JS
// ✅ Correct Version:1 All set v-model
const modelValue = defineModel('modelValue')
// It's possible now:
// - Read: modelValue.value
// - Write: modelValue.value = 'new value' (Automatic emit)
// - watch:watch(modelValue, ...)
// - Parent Component v-model Two-way binding works as usual
10行 → 1行。Vモデルの5つの構成要素により、50行のコードが削減されました。
(3) 収益
defineModel にアップグレードした後:
- コード行数:10行 → 1行(-90%)
- 可読性:1行で意図が明確に伝わる
- 型推論:TypeScriptは自動的に型を推論します
- 拡張性:transform、localStorage などをサポートしています
3. defineModel マクロの詳細な説明
(1) 基本的な使い方
VUE
<!-- Child component:CustomInput.vue -->
<template>
<input :value="modelValue" @input="modelValue = $event.target.value">
</template>
<script setup>
// ✅ 1-line v-model
const modelValue = defineModel('modelValue')
</script>
VUE
<!-- Parent Component:Usage v-model -->
<template>
<CustomInput v-model="searchQuery" />
<p>Search: {{ searchQuery }}</p>
</template>
<script setup>
import { ref } from 'vue'
const searchQuery = ref('')
</script>
(2) 5つの応用例
JS
// 1. Default value
const modelValue = defineModel('modelValue', { default: '' })
// 2. Type Constraints(TypeScript)
const count = defineModel<number>('count', { default: 0 })
// 3. transform Convert(Conversion at the Time of Writing)
const trimmed = defineModel('text', {
set(value) {
return value.trim() // Automatic trim
}
})
// 4. getter(Conversion During Reading)
const upper = defineModel('text', {
get(value) {
return value.toUpperCase() // Always uppercase
}
})
// 5. Several v-model
const name = defineModel('name')
const email = defineModel('email')
const phone = defineModel('phone')
(3) 完全な例
VUE
<!-- Child component:TrimInput.vue -->
<template>
<input :value="value" @input="value = $event.target.value">
</template>
<script setup>
const value = defineModel('value', {
// get: Conversion During Reading
get(val) {
return val?.toUpperCase() || ''
},
// set: Conversion at the Time of Writing
set(val) {
return val?.trim() || ''
},
default: ''
})
</script>
VUE
<!-- Parent Component -->
<CustomInput v-model="name" />
<!-- User Input "alice" → trim Unchanged → uppercase → "ALICE" -->
4. リアクティブプロパティのデストラクチャリング
(1) Vue 3.4 以前の課題点
JS
// ❌ Vue 3.3 and before that
const props = defineProps({ count: Number, name: String })
// Use directly in the template props.count
// JS Key Points props.count
// Deconstruction may result in a loss of response
const { count, name } = props // count/name Convert to a regular value
(2) Vue 3.4:レスポンシブ性を維持しながらのデストラクチャリング
VUE
<!-- Child component -->
<template>
<p>{{ count }} - {{ name }}</p>
<button @click="count++">+1</button>
</template>
<script setup>
// ✅ Vue 3.4:Remains responsive even after deconstruction
const { count, name } = defineProps<{
count: number
name: string
}>()
</script>
分解されたプロパティは引き続き反応性を維持します!これはVue 3.4における最大の改善点の1つです。
(3) 5つの主な活用シナリオ
JS
// 1. Simplify props Usage
const { count, name } = defineProps<{ count: number; name: string }>()
// 2. Deconstruction + Rename
const { count: total, name: userName } = defineProps<{ count: number; name: string }>()
// 3. Deconstruction + Default value
const { count = 0, name = 'Guest' } = defineProps<{ count?: number; name?: string }>()
// 4. In conjunction with watch(After deconstruction watch Still working)
const { count } = defineProps<{ count: number }>()
watch(count, (newVal) => console.log('count changed:', newVal))
// 5. In conjunction with computed(Stay responsive as well)
const { count } = defineProps<{ count: number }>()
const double = computed(() => count * 2)
(4) 重要な注意事項
JS
// ⚠️ Deconstructed props Cannot be reassigned
const { count } = defineProps<{ count: number }>()
count = 10 // ❌ Error:Assignment to constant variable
// ✅ Correct:Via emit Have the parent component modify
const emit = defineEmits(['update:count'])
const handleClick = () => emit('update:count', count + 1)
5. useId 一意のID
(1) なぜ useId が必要なのでしょうか?
SSR(サーバーサイドレンダリング)では、手動で記述されたIDによって競合(複数のコンポーネントインスタンスが同じIDを使用する)が発生する可能性があります。useIdは、グローバルに一意で、SSRに対応したIDを生成します。
JS
// ❌ Old notation:Handwritten ID,SSR There will be a conflict
const id = `input-${Math.random()}`
// ✅ Vue 3.4+:useId
import { useId } from 'vue'
const id = useId() // 'v-0', 'v-1', ...
(2) 5つの主なユースケース
VUE
<!-- 1. Form label Relationship -->
<template>
<label :for="id">Username</label>
<input :id="id" v-model="username">
</template>
<script setup>
import { useId } from 'vue'
const id = useId()
const username = ref('')
</script>
<!-- 2. ARIA Properties -->
<template>
<button :aria-describedby="`${id}-help`">Click</button>
<p :id="`${id}-help`">This button does X</p>
</template>
<!-- 3. Form Error Messages -->
<template>
<input v-model="email" :aria-invalid="!!error" :aria-errormessage="`${id}-error`">
<p v-if="error" :id="`${id}-error`">{{ error }}</p>
</template>
<!-- 4. Tooltip Relationship -->
<template>
<button :aria-describedby="`${id}-tip`">?</button>
<span :id="`${id}-tip`" class="tooltip">Help text</span>
</template>
<!-- 5. Modal Window -->
<template>
<div :id="`${id}-modal`" role="dialog" aria-modal="true">
...
</div>
</template>
(3) 5つの主な利点
| 利点 | 説明 |
|---|---|
| SSRセキュリティ | サーバー/クライアントIDの一致 |
| 一意 | 同じページ内に複数のインスタンスがあっても競合しない |
| 安定版 | コンポーネントの再レンダリングは変更なし |
| 読みやすい | 識別を容易にするため、IDには v- というプレフィックスが付いています |
| 軽量 | 依存関係 0 つ(Vue に組み込まれている) |
6. useTemplateRef(Vue 3.5+ の詳細な解説)
(1) すべての機能
VUE
<template>
<input ref="usernameInput">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
import MyChart from './MyChart.vue'
// ✅ Vue 3.5+:One Name Does It All
const inputRef = useTemplateRef<HTMLInputElement>('usernameInput')
const chartRef = useTemplateRef<InstanceType<typeof MyChart>>('chartComponent')
// ✅ TypeScript Perfect Inference
onMounted(() => {
inputRef.value?.focus() // HTMLInputElement Type
chartRef.value?.refresh() // Component Instance Methods
})
</script>
(2) Vue 3.5 と Vue 3.4 の比較
| ディメンション | Vue 3.4 の ref 変数 | Vue 3.5 の useTemplateRef |
|---|---|---|
| テンプレート参照 | <input ref="inputRef"> |
<input ref="usernameInput"> |
| JS 参照 | const inputRef = ref(null) |
const inputRef = useTemplateRef('usernameInput') |
| 型推論 | 手動 ref<HTMLInputElement | null> |
自動推論 |
| 名称の一貫性 | 誤記の可能性があってもフラグは立てられない | 一貫性がない場合はフラグが立てられる |
| 簡潔さ | 2行 | 1行 |
7. 5 推奨されるアップグレードパス
(1) アップグレードの時期
| Vueのバージョン | リリース日 | ステータス |
|---|---|---|
| Vue 3.0 | 2020年9月 | 旧バージョン(アップグレード推奨) |
| Vue 3.2 | 2021年8月 | 安定版 |
| Vue 3.3 | 2023年5月 | 安定版 |
| Vue 3.4 | 2023年12月 | 現在推奨 |
| Vue 3.5 | 2024年9月 | 最新の安定版 |
(2) アップグレード一覧
TEXT
✅ Vue 3.4:
- defineModel Macro Replacement v-model Complex Notation
- Responsive props Deconstruction
- Abbreviations with the Same Name(<input :value> Replace :value="value")
- Improvements to Error Handling
- Template Parser Override(2x Performance)
✅ Vue 3.5:
- useTemplateRef(Recommendations)
- Responsive props Deconstruction and Improvement
- useId Stable
- Deferred teleport
- Improved Suspense
(3) アップグレードに関する推奨事項
| シナリオ | 推奨事項 |
|---|---|
| 新規プロジェクト | Vue 3.5 以降を直接使用 |
| レガシープロジェクト (Vue 3.3–) | 3.5 へのアップグレード、機能ごとの移行 |
| レガシープロジェクト(Vue 3.0–3.2) | 3.3へ、その後3.5へアップグレード |
| メガプロジェクト | 段階的なアップグレード(まず Vue をアップグレードし、その後 v-model を移行するなど) |
8. 完全な例:Vue 3.4+ を用いた包括的なアプリケーション
(1) ▶ サンプル:. defineModelの5つの活用法
VUE
<!-- 1. Basics -->
<script setup>
const modelValue = defineModel('modelValue')
</script>
<!-- 2. Default value -->
<script setup>
const count = defineModel('count', { default: 0 })
</script>
<!-- 3. TypeScript Type -->
<script setup lang="ts">
const user = defineModel<User>('user', { default: () => ({}) })
</script>
<!-- 4. transform Convert -->
<script setup>
const trimmed = defineModel('text', {
set(val) { return val?.trim() || '' }
})
</script>
<!-- 5. Several v-model -->
<script setup>
const username = defineModel('username')
const password = defineModel('password')
</script>
(2) ▶ サンプル:. リアクティブプロパティのデストラクチャリング
VUE
<template>
<p>{{ count }} - {{ name }}</p>
<button @click="emit('update:count', count + 1)">+1</button>
</template>
<script setup lang="ts">
// ✅ Vue 3.4+:Deconstructing Responsiveness
const { count, name } = defineProps<{
count: number
name: string
}>()
const emit = defineEmits<{
'update:count': [value: number]
}>()
</script>
(3) ▶ サンプル:. useId 5 の主なユースケース
VUE
<template>
<label :for="id">Email</label>
<input :id="id" v-model="email" :aria-invalid="!!error" :aria-errormessage="`${id}-error`">
<p v-if="error" :id="`${id}-error`" class="error">{{ error }}</p>
</template>
<script setup>
import { ref, useId } from 'vue'
const id = useId() // v-0, v-1, ...
const email = ref('')
const error = ref('')
</script>
(4) ▶ サンプル:. useTemplateRef を使用して申請を完了する
VUE
<template>
<input ref="usernameInput" v-model="username">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
import MyChart from './MyChart.vue'
const username = ref('')
const chartData = ref([1, 2, 3])
// ✅ Vue 3.5+:TypeScript Perfect Inference
const inputRef = useTemplateRef<HTMLInputElement>('usernameInput')
const chartRef = useTemplateRef<InstanceType<typeof MyChart>>('chartComponent')
onMounted(() => {
inputRef.value?.focus()
chartRef.value?.refresh()
})
</script>
(5) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| defineModel の分解 | 応答なし | const modelValue = defineModel() を直接使用 |
| props のデストラクチャリング後の代入 | エラー | emit を使用して親が更新できるようにしてください |
| ハードコーディングされた文字列を使用した useId | SSR との競合 | useId() への切り替え |
useTemplateRef の名前が不正です |
ref.value が null です |
名前が一致している必要があります |
| Vue 3.4 以降へのアップグレードには変更点が多すぎる | 互換性の問題 | 段階的な移行 |
(6) ▶ サンプル:. Vue 3.4 以降へのアップグレードによる 5 つの主なメリット
| 収益 | データ |
|---|---|
| コード量の削減 | 30~50%(defineModelの簡素化) |
| パフォーマンスの向上 | テンプレートの解析速度が2倍に |
| 型推論 | 100% 正確 (TypeScript) |
| SSR対応 | useIdによるIDの競合解消 |
| 開発者体験 | DXの大幅な改善 |
❓ よくある質問
Q
defineModel は v-model の構文全体を置き換えるものですか?A はい。
defineModel('modelValue') is equivalent to the 10 lines of code in props.modelValue + emit('update:modelValue') です。完全な下位互換性があります。Q リアクティブなプロップのデストラクチャリングはいつサポートされるようになりますか?
A Vue 3.4以降(2023年12月)。Vue 3.3およびそれ以前のバージョンではサポートされていません。デストラクチャリングされたプロップもリアクティブであり続けます(デフォルトではディープリアクティビティが有効です)。
Q
useId はレンダリングのたびに変わりますか?A いいえ。
useId によって生成される ID は、コンポーネントのライフサイクルを通じて一定です(SSR とクライアント側で一貫しています)。Q useTemplateRef と ref 変数の違いは?
A useTemplateRef は Vue 3.5 以降で導入されました。これにより、TypeScript による型推論が強化され、変数名の変更もより安全に行えるようになりました。一般的なプロジェクトでは、ref 変数で十分です。
Q
defineProps のデストラクチャリングは watch と併用できますか?A はい。
const { count } = defineProps() + watch(count, ...) は問題なく動作します。Q Vue 3.3 から 3.4 以降へアップグレードするにはどうすればよいですか?
A
npm update vue@latest。コードレベルでは、複雑な v-model 構文の代わりに defineModel を使用し、props.xxx の代わりにリアクティブなデストラクチャリングを使用します。互換性の問題は一切ありません。Q Vue 3.5以降では、TypeScriptに関してどのような改善がなされましたか?
A
definePropsによるリアクティブなデストラクチャリング、useTemplateRefによる完全な型推論、そしてマクロに対するより包括的なTypeScriptサポートが追加されました。<script setup lang="ts">の使用をお勧めします。📖 まとめ
defineModelマクロ:1行のv-modelで、10行分の定型コードを置き換える- レスポンシブプロパティの分解:Vue 3.4 以降では、デフォルトでディープレスポンシブが使用されます
- useId: SSRでも安全に使用できる一意のID
- useTemplateRef: Vue 3.5 以降のコンポーザブル ref、強力な型推論
- Vue 3.4 のアップデート:
defineModel、リアクティブ・デストラクチャリング、および同名省略記法 - Vue 3.5 のアップデート:useTemplateRef および useId が安定版になりました
- 推奨:Vue 3.5 以降を直接使用してください
📝 練習問題
(1) 基礎問題(難易度:⭐)
defineModel を使用して v-model コンポーネントをリファクタリングする:
- 子コンポーネント CustomInput.vue:
defineModelの 1 行 - 親コンポーネント:v-model による双方向バインディング
- デフォルト値に対応しています
(2) 上級問題(難易度:⭐⭐)
Vue 3.4 以降でリアクティブ・デストラクチャリングを使用してコンポーネントを書き換える方法:
- count、name などのプロパティを展開する
- 実測値+計算値
- レスポンシブレイアウトのテスト
(3) チャレンジ問題(難易度:⭐⭐⭐)
Vue 3.4+ への完全なアップグレードの例:
- 5つのコンポーネントにおいて、複雑なVモデルの構文を
defineModelに置き換える - リアクティブプロップを使用して3つのコンポーネントを分解する
useIdを使用した 5 つのコンポーネント(labelとinputを接続するため)useTemplateRefを使用した 3 つのコンポーネント(Vue 3.5 以上)- 5 TypeScriptの厳密型付けコンポーネント
- SSRシナリオにおける
useIdの一貫性を検証する



