Vue.js: Vue 3.4 New Features
Last updated: 2026-10-20
Vue 3.4+ (released in December 2023) brings several revolutionary improvements: the defineModel macro simplifies v-model, props destructuring stays reactive, useTemplateRef joins the Composition API, and useId generates unique IDs. These features dramatically simplify template code and improve the developer experience.
This lesson walks you through the 5 core new features so your Vue 3 code becomes shorter and more modern.
1. What You'll Learn
- The
defineModelmacro as a replacement for verbose v-model code - Reactive props destructuring (deeply reactive by default)
useIdfor SSR-safe unique IDsuseTemplateReffor compositional template refs- Improvements to transition animations
- Key differences between Vue 3.4, Vue 3.3, and Vue 3.2
- 5 recommended upgrade paths
2. The "Parent-Child Sync" Headache of 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 that supports v-model:
// ❌ The broken version: v-model needs 10 lines of boilerplate
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 — and every v-model component needs them.
Tech lead:
"Alice, this is too verbose. Vue 3.4's
defineModeldoes it in a single line."
(2) The Vue 3.4 defineModel Solution
// ✅ The correct version: 1 line handles v-model
const modelValue = defineModel('modelValue')
// Now you can:
// - Read: modelValue.value
// - Write: modelValue.value = 'new value' (emits automatically)
// - watch: watch(modelValue, ...)
// - Two-way binding through v-model in the parent keeps working
10 lines → 1 line. Five v-model components save 50 lines of code.
(3) Benefits
After upgrading to defineModel:
- Lines of code: 10 → 1 (-90%)
- Readability: one line makes the intent obvious
- Type inference: TypeScript infers it automatically
- Extensibility: supports transform converters, localStorage, and more
3. defineModel in Depth
(1) Basic Usage
(2) 5 Advanced Usages
// 1. Default value
const modelValue = defineModel('modelValue', { default: '' })
// 2. Type constraint (TypeScript)
const count = defineModel<number>('count', { default: 0 })
// 3. transform (converts on write)
const trimmed = defineModel('text', {
set(value) {
return value.trim() // trims automatically
}
})
// 4. getter (converts on read)
const upper = defineModel('text', {
get(value) {
return value.toUpperCase() // always uppercase
}
})
// 5. Multiple v-models
const name = defineModel('name')
const email = defineModel('email')
const phone = defineModel('phone')
(3) Complete Example
<!-- Parent component -->
<CustomInput v-model="name" />
<!-- User types "alice" → trim leaves it unchanged → uppercase → "ALICE" -->
▶ Example: Hand-Writing v-model in the CDN Build with the :value Same-Name Shorthand
▶ Try it YourselfBrowsers have no SFC compilation, so
defineModelis unavailable here. Below we hand-write what it compiles to —props+emit— and use Vue 3.4's:id/:valuesame-name shorthand along the way.
Output:
The page shows a "Mirror input" label, an input holding vue, the line "Current value: vue", and a second input rendered by the my-input child (also starting at vue).
Typing "3.4" into the first input updates the text to 3.4 immediately, and the child input mirrors it.
Typing "vue3" into the child input updates both the text and the first input — that round trip is exactly the props/emit pair defineModel hides.
▶ Example: Two-Way Binding with v-model (CDN Vue 3.5 Supports the Simplified :value + @input)
▶ Try it Yourself
Output:
Typing in the first input echoes every keystroke in the paragraph underneath, and the (empty) placeholder shows up whenever the field is emptied.
In the second block, clicking "Validate" with an empty or @-less address displays the red error text;
a valid address clears the error and pops an alert.
1. v-model -> Live value: (empty) then Live value: hello
2. validate -> empty input => red "❌ Invalid email format"
-> "alice@mail.com" => alert "✅ Email is valid", error cleared
4. Reactive Props Destructuring
(1) The Pain Before Vue 3.4
// ❌ Vue 3.3 and earlier
const props = defineProps({ count: Number, name: String })
// In the template you use props.count directly
// In JS you must write props.count
// Destructuring loses reactivity
const { count, name } = props // count/name become plain values
(2) Vue 3.4 Keeps Destructured Props Reactive
Destructured props stay reactive! This is one of the biggest improvements in Vue 3.4.
(3) 5 Application Scenarios
// 1. Simplify props usage
const { count, name } = defineProps<{ count: number; name: string }>()
// 2. Destructure + rename
const { count: total, name: userName } = defineProps<{ count: number; name: string }>()
// 3. Destructure + default values
const { count = 0, name = 'Guest' } = defineProps<{ count?: number; name?: string }>()
// 4. With watch (watch still works after destructuring)
const { count } = defineProps<{ count: number }>()
watch(count, (newVal) => console.log('count changed:', newVal))
// 5. With computed (stays reactive too)
const { count } = defineProps<{ count: number }>()
const double = computed(() => count * 2)
(4) Caveats
// ⚠️ Destructured props cannot be reassigned
const { count } = defineProps<{ count: number }>()
count = 10 // ❌ Error: Assignment to constant variable
// ✅ Correct: let the parent change it through emit
const emit = defineEmits(['update:count'])
const handleClick = () => emit('update:count', count + 1)
5. useId for Unique IDs
(1) Why useId?
During SSR (server-side rendering), hand-written IDs collide — several component instances end up using the same ID. useId generates IDs that are globally unique and SSR-safe.
// ❌ Old approach: hand-written ID, collides under SSR
const id = `input-${Math.random()}`
// ✅ Vue 3.4+: useId
const { useId } = Vue
const id = useId() // 'v-0', 'v-1', ...
(2) 5 Usage Scenarios
(3) 5 Advantages
| Advantage | Description |
|---|---|
| SSR-safe | Server and client IDs match |
| Unique | Multiple instances on one page never collide |
| Stable | Unchanged across re-renders |
| Readable | The v- prefix makes IDs easy to spot |
| Lightweight | Zero dependencies (built into Vue) |
6. useTemplateRef (Vue 3.5+ in Depth)
(1) Full Feature Set
<template>
<input ref="usernameInput">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script setup lang="ts">
const { useTemplateRef, onMounted } = 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')
// ✅ Perfect TypeScript inference
onMounted(() => {
inputRef.value?.focus() // HTMLInputElement type
chartRef.value?.refresh() // component instance method
})
</script>
(2) Vue 3.5 vs Vue 3.4
| Aspect | Vue 3.4 variable ref | Vue 3.5 useTemplateRef |
|---|---|---|
| Template ref | <input ref="inputRef"> |
<input ref="usernameInput"> |
| JS ref | const inputRef = ref(null) |
const inputRef = useTemplateRef('usernameInput') |
| Type inference | Requires a manual ref<HTMLInputElement | null> |
Inferred automatically |
| Name consistency | A typo goes unnoticed | A mismatch throws an error |
| Conciseness | 2 lines | 1 line |
7. 5 Upgrade Path Recommendations
(1) Release Timeline
| Vue version | Released | Status |
|---|---|---|
| Vue 3.0 | 2020-09 | Old (upgrade recommended) |
| Vue 3.2 | 2021-08 | Stable |
| Vue 3.3 | 2023-05 | Stable |
| Vue 3.4 | 2023-12 | Currently recommended |
| Vue 3.5 | 2024-09 | Latest stable |
(2) Upgrade Checklist
✅ Vue 3.4:
- defineModel macro replaces verbose v-model code
- Reactive props destructuring
- Same-name shorthand (<input :value> instead of :value="value")
- Improved error handling
- Rewritten template parser (2x faster)
✅ Vue 3.5:
- useTemplateRef (recommended)
- Improved reactive props destructuring
- useId is stable
- Deferred teleport
- Improved Suspense
(3) Upgrade Advice
| Scenario | Recommendation |
|---|---|
| New project | Start on Vue 3.5+ |
| Legacy project (Vue 3.3 or older) | Upgrade to 3.5 and migrate feature by feature |
| Legacy project (Vue 3.0-3.2) | Upgrade to 3.3 first, then to 3.5 |
| Very large project | Incremental upgrade (bump Vue, then migrate v-model usage, etc.) |
8. Putting It Together: Vue 3.4+ Feature Cheat Sheet
▶ Example: Vue 3.4+ Feature Cheat Sheet (⚠️ Requires SFC + Vite, Not Available via CDN)
<script setup> project. The CDN global build of Vue 3 does not expose compile-time macros such as defineModel/useId/useTemplateRef — those macros are replaced with runtime functions when Vite compiles the SFC.
<!-- ⚠️ Requires a Vite project, illustration only -->
<!-- 1. defineModel macro (replaces verbose v-model code) -->
<script setup>
// 1 line replaces 10: props.modelValue + emit('update:modelValue')
const text = defineModel('text', { default: '' })
</script>
<template>
<input v-model="text">
</template>
<!-- 2. Reactive props destructuring (Vue 3.4+) -->
<script setup>
// ✅ Still reactive after destructuring (deep by default)
const { count, name } = defineProps<{
count: number
name: string
}>()
// Use count and name directly; no props.count / props.name needed
</script>
<!-- 3. useId (an SSR-safe unique ID) -->
<script setup>
const { useId } = Vue
const id = useId() // generates v-0, v-1, ... automatically
</script>
<template>
<label :for="id">Email</label>
<input :id="id" type="email">
</template>
<!-- 4. useTemplateRef (strongly typed, Vue 3.5+) -->
<script setup lang="ts">
const { useTemplateRef, onMounted } = Vue
// TypeScript infers HTMLInputElement | null
const inputRef = useTemplateRef<HTMLInputElement>('usernameInput')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="usernameInput" type="text">
</template>
Output:
This snippet renders nothing on its own — it is a side-by-side reference of four features, each written as an isolated <script setup> block.
Mounted in a real Vite project, block 1 gives a two-way bound input, block 2 a count - name paragraph, block 3 a labelled email field with a generated v-0 id, and block 4 an input that focuses itself on mount.
defineModel -> <input> bound to `text`, no props/emit boilerplate
props destructure -> <p>{{ count }} - {{ name }}</p> stays reactive
useId -> <label for="v-0">Email</label><input id="v-0">
useTemplateRef -> input focused on mounted, typed HTMLInputElement | null
(1) 📊 5 Common Mistakes at a Glance
| Mistake | Symptom | Fix |
|---|---|---|
| Destructuring defineModel | Loses reactivity | Use const modelValue = defineModel() directly |
| Assigning to a destructured prop | Runtime error | Use emit to let the parent change it |
| Hard-coding the useId string | SSR collision | Switch to useId() |
| Wrong useTemplateRef name | ref.value is null | The name must match exactly |
| Changing too much when upgrading to 3.4+ | Compatibility problems | Migrate incrementally |
Output:
A five-row troubleshooting table.
Each row names a mistake, the symptom you would actually observe, and the one-line fix — a handy checklist when a migration suddenly stops working.
Renders one table: 5 rows x 3 columns (Mistake | Symptom | Fix).
(2) 📊 5 Upgrade Wins in Vue 3.4+
| Benefit | Data |
|---|---|
| Less code | 30-50% (defineModel simplification) |
| Faster | Template parsing 2x faster |
| Type inference | 100% accurate (TypeScript) |
| SSR-friendly | useId resolves ID collisions |
| Developer experience | DX noticeably improved |
Output:
A five-row table summarizing the measurable wins of the upgrade;
the percentages are typical project-level estimates rather than a guarantee for every codebase.
Renders one table: 5 rows x 2 columns (Benefit | Data).
❓ FAQ
defineModel('modelValue') is equivalent to the 10 lines of props.modelValue + emit('update:modelValue'). It is fully backwards compatible.const { count } = defineProps() plus watch(count, ...) works perfectly.npm update vue@latest. In code: defineModel replaces verbose v-model code, and reactive destructuring replaces props.xxx. No breaking changes.<script setup lang="ts">.📖 Summary
- defineModel macro: 1 line of v-model instead of 10 lines of boilerplate
- Reactive props destructuring: deeply reactive by default in Vue 3.4+
- useId: SSR-safe unique IDs
- useTemplateRef: compositional refs in Vue 3.5+, with strong type inference
- Vue 3.4 upgrade: defineModel + reactive destructuring + same-name shorthand
- Vue 3.5 upgrade: useTemplateRef + stable useId
- Recommendation: start directly on Vue 3.5+
📝 Exercises
-
Basic (difficulty ⭐) Refactor a v-model component with defineModel:
- Child component CustomInput.vue: a single defineModel line
- Parent component: two-way binding through v-model
- Support a default value
-
Intermediate (difficulty ⭐⭐) Refactor a component with Vue 3.4+ reactive destructuring:
- Destructure props such as count and name
- Combine with watch and computed
- Verify that reactivity is preserved
-
Challenge (difficulty ⭐⭐⭐) Build a complete Vue 3.4+ upgrade demo:
- 5 components using defineModel instead of verbose v-model code
- 3 components using reactive props destructuring
- 5 components using useId (label/input association)
- 3 components using useTemplateRef (Vue 3.5+)
- 5 strongly typed TypeScript components
- A test that useId stays consistent under SSR