Introduction
Vue.js has come a long way since its initial release in 2014. Vue 3 introduced the Composition API and Script Setup, fundamentally changed how developers write Vue applications. Now, Vue 4 takes everything to the next level with Vapor Mode, improved reactivity, and a host of developer experience improvements.
In this comprehensive guide, we’ll explore everything you need to know about Vue 4. From the revolutionary Vapor Mode that eliminates the virtual DOM overhead to the new devtools features, you’ll learn how to build faster, more efficient applications with Vue 4.
What’s New in Vue 4
The Big Changes
Vue 4 introduces several groundbreaking features:
| Feature | Description | Impact |
|---|---|---|
| Vapor Mode | Eliminated virtual DOM for select components | 10-100x performance improvement |
| Improved Reactivity | Fine-grained reactivity with proxies | Faster updates, less memory |
| Native Props | TypeScript-first props without runtime overhead | Better TypeScript support |
| Improved DevTools | Better debugging experience | Faster development |
Why Vapor Mode Matters
The biggest change in Vue 4 is Vapor Mode. Unlike the traditional Vue rendering pipeline that uses a virtual DOM, Vapor Mode compiles your templates to direct DOM operations:
// Traditional Vue 3 (virtual DOM)
// Updates require diffing and reconciliation
// Vue 4 Vapor Mode (direct DOM)
// Compiles to efficient direct mutations
This means:
- No virtual DOM overhead
- Automatic batching of updates
- Better memory efficiency
- Near-native performance
Getting Started with Vue 4
Installation
Create a new Vue 4 project:
# Using npm
npm create vue@latest my-vue-app
cd my-vue-app
npm install
# Select Vue 4 when prompted
Project Structure
my-vue-app/
โโโ src/
โ โโโ components/
โ โ โโโ Counter.vue
โ โโโ composables/
โ โ โโโ useCounter.ts
โ โโโ App.vue
โ โโโ main.ts
โโโ index.html
โโโ package.json
โโโ vite.config.ts
Vue 4 Basics
Script Setup Improvements
Vue 4’s Script Setup is even more powerful:
<script setup lang="ts">
// Define props with type inference
const { count = 0, label = 'Counter' } = defineProps<{
count?: number
label?: string
}>()
// Emit events with full typing
const emit = defineEmits<{
update: [value: number]
reset: []
}>()
// Reactive state
const value = $ref(count)
const doubled = $derived(value * 2)
// Methods
function increment() {
value++
emit('update', value)
}
function reset() {
value = 0
emit('reset')
}
</script>
<template>
<div class="counter">
<h2>{{ label }}</h2>
<p>Count: {{ value }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</div>
</template>
Understanding Vapor Mode
How Vapor Mode Works
Vapor Mode is optional in Vue 4. You can enable it per-component:
<script setup vapor>
import { ref, computed } from 'vue/vapor'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
</div>
</template>
When to Use Vapor Mode
Vapor Mode is ideal for:
- Performance-critical components
- List rendering with large datasets
- Real-time applications
- Animation-heavy interfaces
Vapor vs Traditional Mode
| Aspect | Traditional | Vapor |
|---|---|---|
| Virtual DOM | Yes | No |
| Performance | Good | Excellent |
| Memory | Moderate | Low |
| Compatibility | Full | Partial |
| Bundle Size | ~50KB | ~30KB |
Reactivity System
New Reactivity Primitives
Vue 4 introduces new reactivity primitives:
// $ref - creates a reactive reference
const count = $ref(0)
// $computed - creates a computed value
const doubled = $computed(() => count.value * 2)
// $effect - creates a side effect
$effect(() => {
console.log(`Count is now: ${count.value}`)
})
// $watch - watches for changes
$watch(count, (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`)
})
// $shallowRef - creates a shallow reactive reference
const state = $shallowRef({
nested: { value: 0 }
})
Fine-Grained Reactivity
Vue 4’s reactivity is more granular:
import { $ref, $effect } from 'vue'
// Only triggers updates when specifically accessed
const state = $ref({
user: {
profile: {
name: 'John',
email: '[email protected]'
}
}
})
// Only re-renders when name changes
$effect(() => {
console.log(state.user.profile.name)
})
Components in Vue 4
New Component Features
Async Components
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
<template>
<HeavyComponent v-if="show" />
</template>
Suspense
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
State Management
Pinia in Vue 4
Pinia remains the recommended state management solution:
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
history: [] as number[]
}),
getters: {
doubled: (state) => state.count * 2,
average: (state) =>
state.history.length > 0
? state.history.reduce((a, b) => a + b, 0) / state.history.length
: 0
},
actions: {
increment() {
this.count++
this.history.push(this.count)
},
decrement() {
this.count--
this.history.push(this.count)
},
reset() {
this.count = 0
this.history = []
}
}
})
Using the Store
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ store.count }}</p>
<p>Doubled: {{ store.doubled }}</p>
<button @click="store.increment">+</button>
<button @click="store.decrement">-</button>
<button @click="store.reset">Reset</button>
</div>
</template>
TypeScript Integration
Full TypeScript Support
Vue 4 has native TypeScript support:
// types/User.ts
interface User {
id: number
name: string
email: string
role: 'admin' | 'user' | 'guest'
}
// components/UserCard.vue
<script setup lang="ts">
import type { User } from '@/types/User'
interface Props {
user: User
showEmail?: boolean
}
const { user, showEmail = false } = defineProps<Props>()
const emit = defineEmits<{
select: [user: User]
delete: [id: number]
}>()
function handleSelect() {
emit('select', user)
}
</script>
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p v-if="showEmail">{{ user.email }}</p>
<span class="badge">{{ user.role }}</span>
<button @click="handleSelect">Select</button>
</div>
</template>
Vue Router 5
New Router Features
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue')
},
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/UserView.vue'),
props: true,
children: [
{
path: 'profile',
name: 'user-profile',
component: () => import('@/views/UserProfile.vue')
}
]
}
]
})
export default router
Navigation Guards
router.beforeEach((to, from, next) => {
const isAuthenticated = useAuthStore().isAuthenticated
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
Best Practices
Performance Optimization
<script setup>
// Use shallowRef for large objects
const largeData = $shallowRef(externalLibrary.loadData())
// Use computed for derived state
const sortedItems = $computed(() =>
[...items.value].sort((a, b) => a.name.localeCompare(b.name))
)
// Memoize expensive computations
import { useMemo } from 'vue-vapor'
const expensiveResult = useMemo(() =>
heavyComputation(data.value)
, [data])
</script>
<template>
<!-- Use v-memo for list performance -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.updated]"
>
{{ item.content }}
</div>
</template>
Component Design
<!-- Good: Single Responsibility -->
<template>
<UserAvatar :src="user.avatar" :alt="user.name" />
<UserName :name="user.name" :verified="user.verified" />
<UserBio :bio="user.bio" />
</template>
<!-- Avoid: God Components -->
<template>
<!-- Don't put everything in one component -->
<UserProfile :user="user" />
</template>
Migration from Vue 3
Key Changes
| Vue 3 | Vue 4 |
|---|---|
ref() |
$ref() |
computed() |
$computed() |
watch() |
$watch() |
reactive() |
$state() |
defineProps() |
defineProps<Props>() |
Migration Steps
- Update dependencies:
npm install vue@4 vue-router@5 pinia@3
- Update components:
<!-- Vue 3 -->
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
<!-- Vue 4 -->
<script setup>
const count = $ref(0)
const doubled = $computed(() => count * 2)
</script>
- Enable Vapor Mode (optional):
<script setup vapor>
// Components now use Vapor rendering
</script>
External Resources
Official Documentation
Learning Resources
Tools
- Vite - Build tool
- Vue DevTools - Browser extension
- Volta - JS tool manager
Conclusion
Vue 4 represents a significant evolution in the Vue ecosystem. The introduction of Vapor Mode brings performance that rivals vanilla JavaScript, while the improved developer experience makes building applications more enjoyable than ever.
Key takeaways:
- Vapor Mode is optional but powerful: Enable it for performance-critical components
- New reactivity primitives: Use $ref, $computed, $effect for cleaner code
- TypeScript is first-class: Full type inference out of the box
- Migration is straightforward: Most Vue 3 code will work with minimal changes
Whether you’re building a small widget or a large-scale application, Vue 4 provides the tools you need to ship faster with excellent performance.
Related Articles
- Svelte 5 Complete Guide
- Qwik Complete Guide: Zero-Hydration Framework
- Astro Complete Guide: The Static-First Framework
- HTMX Complete Guide: Simpler React Alternative
Comments