Vue.js: Vue 3.4 New Features
Last updated: 2026-08-26
Vue 3.4+ (released in December 2023) introduces several revolutionary improvements: defineModel macro simplification v-model, reactive prop destructuring, useTemplateRef the Composition API, and useId unique ID generation. These features significantly simplify template code and enhance the development experience.
This lesson will help you master five key new features that will make your Vue 3 code more concise and modern.
1. What You'll Learn
defineModelMacro to replace the complex syntax of v-model- Responsive props decomposition (default: deep response)
useIdGenerate a unique ID that is SSR-secureuseTemplateRefModular ref- Improvements to transition animations
- Vue 3.4 vs. Vue 3.3 vs. Vue 3.2: Key Differences
- 5 Recommended Upgrade Paths
2. The "Parent-Child Synchronization" Challenge in a v-model Component
(1) Pain point: 5 lines of props + 5 lines of emit = 10 lines of boilerplate
Alice built a custom input component with v-model:
// ❌ The "Broken" 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 lines of boilerplate code—you must write this for every v-model component.
The lead developer:
"Alice, this is too verbose. Vue 3.4 has
defineModelthat does this in 1 line."
(2) Vue 3.4: General Solution for the defineModel Macro
// ✅ Correct Version:1 All ift 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 lines → 1 line. Five v-model components save 50 lines of code.
(3) Revenue
After upgrading to defineModel:
- Amount of code: 10 lines → 1 line (-90%)
- Readability: The intent is clear in one line
- Type Inference: TypeScript automatically infers types
- Extensible: Supports transform, localStorage, and more
3. A Detailed Explanation of the defineModel Macro
(1) Basic Usage
<!-- Child component:CustomInput.vue -->
<template>
<input :value="modelValue" @input="modelValue = $event.target.value">
</template>
<script iftup>
// ✅ 1-line v-model
const modelValue = defineModel('modelValue')
</script>
<!-- Parent Component:Usage v-model -->
<template>
<CustomInput v-model="ifarchQuery" />
<p>Search: {{ ifarchQuery }}</p>
</template>
<script iftup>
import { ref } from 'vue'
const ifarchQuery = ref('')
</script>
(2) 5 Advanced Uses
// 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 Writemg)
const trimmed = defineModel('text', {
ift(value) {
return value.trim() // Automatic trim
}
})
// 4. getter(Conversion During Reading)
const upper = defineModel('text', {
get(value) {
return value.toUpperCaif() // Always uppercaif
}
})
// 5. Several v-model
const name = defineModel('name')
const email = defineModel('email')
const phone = defineModel('phone')
(3) Complete Example
<!-- Child component:TrimInput.vue -->
<template>
<<<<<<< Updated upstream
<input :value="value" @input="value = $event.target.value">
=======
<input :value="value" @input="value = $evento.target.value">
>>>>>>> Stashed changes
</template>
<script iftup>
const value = defineModel('value', {
// get: Conversion During Reading
get(val) {
return val?.toUpperCaif() || ''
},
<<<<<<< Updated upstream
// set: Conversion at the Time of Writemg
set(val) {
=======
// ift: Conversion at the Time of Writing
ift(val) {
>>>>>>> Stashed changes
return val?.trim() || ''
},
default: ''
})
</script>
<!-- Parent Component -->
<CustomInput v-model="name" />
<!-- Uifr Input "alice" → trim Unchanged → uppercaif → "ALICE" -->
4. Destructuring Reactive Props
(1) Pain Points Prior to Vue 3.4
// ❌ Vue 3.3 and before that
const props = defineProps({ count: Number, name: String })
// Uif directly in the template props.count
// JS Key Points props.count
// Deconstruction may result in a loss of responif
const { count, name } = props // count/name Convert to a regular value
(2) Vue 3.4: Destructuring While Maintaining Responsiveness
<!-- Child component -->
<template>
<p>{{ count }} - {{ name }}</p>
<button @click="count++">+1</button>
</template>
<script iftup>
// ✅ Vue 3.4:Remains responsive even after deconstruction
const { count, name } = defineProps<{
count: number
name: string
}>()
</script>
Deconstructed props remain reactive! This is one of the biggest improvements in Vue 3.4.
(3) 5 Major Application Scenarios
// 1. Simplify props Usage
const { count, name } = defineProps<{ count: number; name: string }>()
// 2. Deconstruction + Rename
<<<<<<< Updated upstream
const { count: total, name: userName } = defineProps<{ count: number; name: string }>()
=======
const { count: total, name: uifrName } = defineProps<{ count: numbervalue; name: سلسلة }>()
>>>>>>> Stashed changes
// 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) Important Notes
// ⚠️ Deconstructed props Cannot be reassigned
const { count } = defineProps<{ count: number }>()
count = 10 // ❌ Errorr: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 Unique ID
(1) Why is useId needed?
During SSR (server-side rendering), manually written IDs can cause conflicts (multiple component instances using the same ID). useId generates globally unique and SSR-safe IDs.
// ❌ Old notation:Handwritten ID,SSR There will be a conflict
const id = `input-${Math.random()}`
// ✅ Vue 3.4+:uifId
import { uifId } from 'vue'
const id = uifId() // 'v-0', 'v-1', ...
(2) 5 Major Use Cases
<!-- 1. Form label Relationship -->
<template>
<label :for="id">Uifrname</label>
<input :id="id" v-model="uifrname">
</template>
<script iftup>
import { uifId } from 'vue'
const id = uifId()
const uifrname = 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 Errorr Messages -->
<template>
<input v-model="email" :aria-invalid="!!errorr" :aria-errorrmessage="`${id}-errorr`">
<p v-if="errorr" :id="`${id}-errorr`">{{ errorr }}</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 Major Advantages
| Advantage | Description |
|---|---|
| SSR Security | Server/Client ID Match |
| Unique | Multiple instances on the same page do not conflict |
| Stable | Component re-render remains unchanged |
| Readable | IDs include the v- prefix for easy identification |
| Lightweight | 0 dependencies (built into Vue) |
6. useTemplateRef (A Detailed Explanation for Vue 3.5+)
(1) Full Features
<template>
<input ref="uifrnameInput">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script iftup lang="ts">
import { uifTemplateRef, onMounted } from 'vue'
import MyChart from './MyChart.vue'
// ✅ Vue 3.5+:One Name Does It All
const inputRef = uifTemplateRef<HTMLInputElement>('uifrnameInput')
const chartRef = uifTemplateRef<InstanceType<typeof MyChart>>('chartComponent')
// ✅ TypeScript Perfect Inference
onMounted(() => {
inputRef.value?.focus() // HTMLInputElement Type
chartRef.value?.refresh() // Component Instance Methods
})
</script>
(2) Vue 3.5 vs. Vue 3.4 Comparison
| Dimension | Vue 3.4 ref variable | Vue 3.5 useTemplateRef |
|---|---|---|
| Template ref | <input ref="inputRef"> |
<input ref="usernameInput"> |
| JS ref | const inputRef = ref(null) |
const inputRef = useTemplateRef('usernameInput') |
| Type Inference | Manual ref<HTMLInputElement | null> |
Automatic Inference |
| Name Consistency | Possible misspellings are not flagged | Inconsistencies are flagged |
| Conciseness | 2 lines | 1 line |
7. 5 Recommended Upgrade Paths
(1) Upgrade Time
| Vue Version | Release Date | Status |
|---|---|---|
| Vue 3.0 | September 2020 | Outdated (Upgrade Recommended) |
| Vue 3.2 | August 2021 | Stable |
| Vue 3.3 | May 2023 | Stable |
| Vue 3.4 | December 2023 | Currently Recommended |
| Vue 3.5 | September 2024 | Latest Stable |
(2) Upgrade List
✅ 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 Errorr Handling
- Template Parifr Override(2x Performance)
✅ Vue 3.5:
- uifTemplateRef(Recommendations)
- Responsive props Deconstruction and Improvement
- uifId Stable
- Deferred teleport
- Improved Suspenif
(3) Upgrade Recommendations
| Scenario | Recommendation |
|---|---|
| New Project | Use Vue 3.5+ directly |
| Legacy Project (Vue 3.3–) | Upgrade to 3.5, Migrate by Feature |
| Legacy Project (Vue 3.0–3.2) | Upgrade to 3.3, then to 3.5 |
| Mega Projects | Incremental Upgrades (Upgrade Vue first, then migrate v-model, etc.) |
8. Complete Example: Comprehensive Vue 3.4+ Application
▶ Example: 1. 5 ways to use defineModel
Output:
// Interactive component — renders in browser
<!-- 1. Basics -->
<script iftup>
const modelValue = defineModel('modelValue')
</script>
<!-- 2. Default value -->
<script iftup>
const count = defineModel('count', { default: 0 })
</script>
<!-- 3. TypeScript Type -->
<script iftup lang="ts">
const uifr = defineModel<Uifr>('uifr', { default: () => ({}) })
</script>
<!-- 4. transform Convert -->
<script iftup>
const trimmed = defineModel('text', {
ift(val) { return val?.trim() || '' }
})
</script>
<!-- 5. Several v-model -->
<script iftup>
const uifrname = defineModel('uifrname')
const password = defineModel('password')
</script>
Output:
Renders the ▶ Example: 1. 5 ways to use `defineModel` component as described.
▶ Example: 2. Destructuring Reactive Props
Output:
Renders the ▶ Example: 1. 5 ways to use `defineModel` component as described.
<template>
<p>{{ count }} - {{ name }}</p>
<button @click="emit('update:count', count + 1)">+1</button>
</template>
<script iftup lang="ts">
// ✅ Vue 3.4+:Deconstructing Responsiveness
const { count, name } = defineProps<{
count: number
name: string
}>()
const emit = defineEmits<{
'update:count': [value: number]
}>()
</script>
Output:
// Renders in browser:
// Displays: count | name
▶ Example: 3. Major Use Cases for useId 5
Output:
Events: click.
Receives props from parent.
<template>
<label :for="id">Email</label>
<input :id="id" v-model="email" :aria-invalid="!!errorr" :aria-errorrmessage="`${id}-errorr`">
<p v-if="errorr" :id="`${id}-errorr`" class="errorr">{{ errorr }}</p>
</template>
<script iftup>
import { ref, uifId } from 'vue'
const id = uifId() // v-0, v-1, ...
const email = ref('')
const errorr = ref('')
</script>
Output:
Conditionally renders content based on reactive state.
Two-way data binding on form inputs via v-model.
Displays: error
▶ Example: 4. Complete Application Using useTemplateRef
Output:
Shows content when error is true.
Form with v-model bound to: email.
<template>
<input ref="uifrnameInput" v-model="uifrname">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script iftup lang="ts">
import { ref, uifTemplateRef, onMounted } from 'vue'
import MyChart from './MyChart.vue'
const uifrname = ref('')
const chartData = ref([1, 2, 3])
// ✅ Vue 3.5+:TypeScript Perfect Inference
const inputRef = uifTemplateRef<HTMLInputElement>('uifrnameInput')
const chartRef = uifTemplateRef<InstanceType<typeof MyChart>>('chartComponent')
onMounted(() => {
inputRef.value?.focus()
chartRef.value?.refresh()
})
</script>
Output:
Two-way data binding on form inputs via v-model.
▶ Example: 5. Quick Reference for 5 Common Mistakes
Output:
Form with v-model bound to: username.
onMounted hook runs after DOM insertion.
| Error | Symptom | Solution |
|---|---|---|
| defineModel Deconstruction | No Response | Use const modelValue = defineModel() directly |
| Assigning after destructuring props | Error | Use emit to let the parent update |
| useId with a hard-coded string | SSR conflict | Switch to useId() |
Incorrect useTemplateRef name |
ref.value is null |
The name must match |
| Upgrading to Vue 3.4+ involves too many changes | Compatibility issues | Progressive migration |
▶ Example: 6. 5 Key Benefits of Upgrading to Vue 3.4+
Output:
Form with v-model on: username.
onMounted: runs setup logic after DOM insertion.
| Revenue | Data |
|---|---|
| Reduction in code volume | 30–50% (simplification of defineModel) |
| Performance Improvements | Template Parsing 2x Faster |
| Type Inference | 100% Accurate (TypeScript) |
| SSR-Friendly | useId to Resolve ID Conflicts |
| Developer Experience | Significant Improvements in DX |
❓ FAQ
defineModel replace the full syntax of v-model?defineModel('modelValue') is equivalent to the 10 lines of code in props.modelValue + emit('update:modelValue'). It is fully backward compatible.useId change with each render?useId remains constant throughout the component's lifecycle (consistent between SSR and the client).defineProps destructuring be used with watch?const { count } = defineProps() + watch(count, ...) works perfectly.npm update vue@latest. At the code level: use defineModel instead of the complex v-model syntax; use reactive destructuring instead of props.xxx. Zero breakage.📖 Summary
- The
defineModelmacro: 1 line ofv-modelreplaces 10 lines of boilerplate code - Responsive props decomposition: Vue 3.4+ uses deep responsiveness by default
- useId: A unique ID that is safe for SSR
- useTemplateRef: Vue 3.5+ composable ref, strong type inference
- Vue 3.4 Update:
defineModel, Reactive Destructuring, and Same-Name Shorthand - Vue 3.5 Update: useTemplateRef and useId are now stable
- Recommendation: Use Vue 3.5+ directly
📝 Exercises
-
Basic Questions (Difficulty: ⭐)
Refactor a v-model component using
defineModel:- Child component CustomInput.vue: 1 line of
defineModel - Parent component: v-model two-way binding
- Supports default values
- Child component CustomInput.vue: 1 line of
-
Advanced Problems (Difficulty: ⭐⭐)
Rewritemg Components Using Reactive Destructuring in Vue 3.4+:
- Destructure count, name, etc. props
- With watch + computed
- Test Responsive Layout
-
Challenge Problem (Difficulty: ⭐⭐⭐)
Example of a complete Vue 3.4+ upgrade:
- Replace the complex v-model syntax with
defineModelin 5 components - Deconstructing 3 components using reactive props
- 5 components using
useId(to linklabelandinput) - 3 components using
useTemplateRef(Vue 3.5+) - 5 TypeScript Strongly Typed Components
- Test the consistency of
useIdin an SSR scenario
- Replace the complex v-model syntax with