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.

Related Directives:v-model

1. What You'll Learn


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:

JS
// ❌ 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 defineModel does it in a single line."

(2) The Vue 3.4 defineModel Solution

JS
// ✅ 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:


3. defineModel in Depth

(1) Basic Usage

(2) 5 Advanced Usages

JS
// 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

VUE
<!-- 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

Browsers have no SFC compilation, so defineModel is unavailable here. Below we hand-write what it compiles to — props + emit — and use Vue 3.4's :id / :value same-name shorthand along the way.

▶ Try it Yourself

Output:

TEXT
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:

TEXT
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.
TEXT
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

JS
// ❌ 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

JS
// 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

JS
// ⚠️ 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.

JS
// ❌ 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

VUE
<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

TEXT
✅ 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)

⚠️ The examples below must run inside a Vite + <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.

VUE
<!-- ⚠️ 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>
▶ Try it Yourself

Output:

TEXT
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.
TEXT
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:

TEXT
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.
TEXT
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:

TEXT
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.
TEXT
Renders one table: 5 rows x 2 columns (Benefit | Data).

Related Directives:v-model

❓ FAQ

Q Does defineModel fully replace the verbose v-model pattern?
A Yes. defineModel('modelValue') is equivalent to the 10 lines of props.modelValue + emit('update:modelValue'). It is fully backwards compatible.
Q When did reactive props destructuring land?
A Vue 3.4+ (December 2023). Vue 3.3 and earlier do not support it. Destructured props stay reactive, deeply by default.
Q Does useId change on every render?
A No. The ID generated by useId stays stable for the lifetime of the component, and it matches between server and client.
Q useTemplateRef vs a ref variable?
A useTemplateRef arrived in Vue 3.5+, with stronger TypeScript inference and safer renaming. A plain ref variable is fine for most projects.
Q Can destructured defineProps be used with watch?
A Yes. const { count } = defineProps() plus watch(count, ...) works perfectly.
Q How do I upgrade from Vue 3.3 to 3.4+?
A Run npm update vue@latest. In code: defineModel replaces verbose v-model code, and reactive destructuring replaces props.xxx. No breaking changes.
Q What does Vue 3.5+ improve for TypeScript?
A Reactive defineProps destructuring, perfect type inference for useTemplateRef, and better TS support for macros. Prefer <script setup lang="ts">.

📖 Summary


📝 Exercises

  1. 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
  2. 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
  3. Challenge (difficulty ⭐⭐⭐) Build a complete Vue 3.4+ upgrade demo:

    1. 5 components using defineModel instead of verbose v-model code
    2. 3 components using reactive props destructuring
    3. 5 components using useId (label/input association)
    4. 3 components using useTemplateRef (Vue 3.5+)
    5. 5 strongly typed TypeScript components
    6. A test that useId stays consistent under SSR
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏