Introduction
Vue 3 represents a significant evolution in JavaScript frameworks, built from the ground up with performance and developer experience in mind. What sets Vue apart isn’t just its gentle learning curve, but how it scales from simple interactive widgets to complex single-page applications without forcing you to rewrite everything.
This guide walks through Vue 3’s core concepts and practical implementation, giving you the foundation to build real applications. We’ll cover the essential patterns you’ll use daily and the gotchas that trip up most developers starting out.
Why Choose Vue 3?
Vue 3 isn’t just an incremental update. The rewrite introduced fundamental improvements that address real-world development pain points:
Composition API: The biggest game-changer. Instead of scattering related logic across data, methods, and computed, you can group functionality together. This becomes crucial as components grow beyond simple examples.
Better TypeScript Support: Vue 3 was written in TypeScript, so the type inference actually works. No more fighting with complex type definitions just to get autocomplete.
Performance Gains: The new reactivity system is faster and uses less memory. Tree-shaking means your bundles only include what you actually use.
Fragment Support: You can finally return multiple root elements without wrapping them in a useless <div>.
💡 Tip: Don’t feel pressured to use the Composition API immediately. The Options API still works and is perfectly fine for most use cases. Composition API shines when you need to share logic between components or handle complex state.
Setting Up Vue 3
Prerequisites
You’ll need Node.js 16+ for the best experience. While Vue works with older versions, you’ll miss out on some tooling improvements.
Installation Options
Vite is the Way Forward
Skip Vue CLI for new projects. Vite is faster, simpler, and officially recommended:
npm create vue@latest my-project
cd my-project
npm install
npm run dev
The Vue create command gives you options for TypeScript, router, testing, and linting. Start minimal and add what you need.
Quick Prototyping with CDN
For experimenting or adding Vue to existing pages:
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
Vue.createApp({
data() {
return { message: 'Hello Vue!' }
}
}).mount('#app')
</script>
⚠️ Warning: CDN builds include the full compiler and are larger. Use the runtime-only build for production.
Creating Your First Vue 3 Component
Understanding Single File Components
Vue’s Single File Component (SFC) format keeps related code together while maintaining clear separation:
<template>
<div class="greeting">
<h1>{{ displayMessage }}</h1>
<button @click="toggleMessage" :disabled="isLoading">
{{ buttonText }}
</button>
</div>
</template>
<script>
export default {
name: 'GreetingCard',
data() {
return {
message: "Welcome to Vue 3!",
isToggled: false,
isLoading: false
};
},
computed: {
displayMessage() {
return this.isToggled ? "Thanks for clicking!" : this.message;
},
buttonText() {
return this.isLoading ? "Loading..." : "Click Me";
}
},
methods: {
async toggleMessage() {
this.isLoading = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
this.isToggled = !this.isToggled;
this.isLoading = false;
}
}
};
</script>
<style scoped>
.greeting {
text-align: center;
padding: 2rem;
}
h1 {
color: #42b983;
margin-bottom: 1rem;
}
button {
background-color: #42b983;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: opacity 0.2s;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #369870;
}
</style>
Key Concepts Explained
Reactivity: When isToggled changes, displayMessage automatically recalculates and the DOM updates. This happens because Vue tracks dependencies during the initial render.
Computed vs Methods: Computed properties cache their results and only recalculate when dependencies change. Methods run every time they’re called.
Event Handling: @click is shorthand for v-on:click. Vue automatically passes the event object if your method needs it.
📌 Note: The
nameproperty isn’t required but helps with debugging in Vue DevTools.
The Composition API
The Composition API solves the problem of logic scattered across different sections in larger components. Here’s the same component using Composition API:
<template>
<div class="greeting">
<h1>{{ displayMessage }}</h1>
<button @click="toggleMessage" :disabled="isLoading">
{{ buttonText }}
</button>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
name: 'GreetingCard',
setup() {
// Reactive state
const message = ref("Welcome to Vue 3!");
const isToggled = ref(false);
const isLoading = ref(false);
// Computed properties
const displayMessage = computed(() =>
isToggled.value ? "Thanks for clicking!" : message.value
);
const buttonText = computed(() =>
isLoading.value ? "Loading..." : "Click Me"
);
// Methods
const toggleMessage = async () => {
isLoading.value = true;
await new Promise(resolve => setTimeout(resolve, 500));
isToggled.value = !isToggled.value;
isLoading.value = false;
};
return {
displayMessage,
buttonText,
toggleMessage,
isLoading
};
}
};
</script>
Composition API Benefits
Logical Grouping: Related state and functions stay together, making complex components easier to understand.
Reusability: Extract common logic into composables (Vue’s version of React hooks):
// composables/useToggle.js
import { ref } from 'vue';
export function useToggle(initialValue = false) {
const isToggled = ref(initialValue);
const toggle = () => isToggled.value = !isToggled.value;
return { isToggled, toggle };
}
Better TypeScript: Type inference works naturally without complex generic gymnastics.
⚠️ Warning: Don’t destructure reactive objects directly. Use
toRefs()if you need to destructure refs from a reactive object.
Adding Vue Router
Vue Router handles client-side navigation and is essential for any multi-page application. Here’s a practical setup:
Installation and Basic Setup
npm install vue-router@4
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
import NotFound from '@/views/NotFound.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'), // Lazy loading
props: true // Pass route params as props
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
];
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0 };
}
});
export default router;
Navigation and Route Guards
<template>
<nav>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link :to="{ name: 'User', params: { id: 123 } }">
User 123
</router-link>
</nav>
<router-view />
</template>
Programmatic Navigation:
// Inside component
this.$router.push('/about');
this.$router.push({ name: 'User', params: { id: 456 } });
// With Composition API
import { useRouter } from 'vue-router';
export default {
setup() {
const router = useRouter();
const goToUser = (id) => {
router.push({ name: 'User', params: { id } });
};
return { goToUser };
}
};
💡 Tip: Use named routes instead of path strings. They’re less error-prone and easier to refactor.
Route Guards and Meta Fields
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
}
];
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login');
} else {
next();
}
});
Component Communication
Props and Events
Props flow down, events flow up. This unidirectional data flow prevents bugs:
<!-- Parent.vue -->
<template>
<UserCard
:user="selectedUser"
:loading="isLoading"
@update="handleUserUpdate"
@delete="handleUserDelete"
/>
</template>
<script>
export default {
data() {
return {
selectedUser: { name: 'John', email: 'john@example.com' },
isLoading: false
};
},
methods: {
handleUserUpdate(updatedUser) {
this.selectedUser = { ...this.selectedUser, ...updatedUser };
},
async handleUserDelete() {
this.isLoading = true;
await this.deleteUser(this.selectedUser.id);
this.isLoading = false;
}
}
};
</script>
<!-- UserCard.vue -->
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="editUser" :disabled="loading">Edit</button>
<button @click="deleteUser" :disabled="loading">Delete</button>
</div>
</template>
<script>
export default {
props: {
user: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
emits: ['update', 'delete'],
methods: {
editUser() {
this.$emit('update', { name: 'Updated Name' });
},
deleteUser() {
this.$emit('delete');
}
}
};
</script>
⚠️ Warning: Always define your emits. Vue 3 will warn about undefinedemits in development, and it helps with debugging.
Styling with Vue
Scoped Styles and CSS Modules
Scoped styles prevent style leakage:
<style scoped>
.button {
background: blue;
}
/* Compiles to .button[data-v-f3f3eg9] */
</style>
CSS Modules for programmatic access:
<template>
<button :class="$style.primaryButton">Click me</button>
</template>
<style module>
.primaryButton {
background: blue;
color: white;
}
</style>
Dynamic Styling
<template>
<div :class="{ active: isActive, 'text-danger': hasError }">
<p :style="{ color: textColor, fontSize: fontSize + 'px' }">
Dynamic content
</p>
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false,
textColor: '#333',
fontSize: 16
};
}
};
</script>
💡 Tip: For multiple classes, use an array:
:class="[baseClass, { active: isActive }]"
Building and Deployment
Optimizing for Production
npm run build
The build process:
- Tree-shaking: Removes unused code
- Minification: Reduces file sizes
- Asset optimization: Optimizes images and other assets
- Code splitting: Creates separate chunks for better caching
Environment Variables
# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My Vue App
// In your app
const apiUrl = import.meta.env.VITE_API_URL;
📌 Note: Only variables prefixed with
VITE_are exposed to your app code.
Deployment Strategies
Static Hosting (Netlify, Vercel):
- Build outputs static files
- Great for most Vue apps
- Easy CI/CD integration
Server-Side Rendering (Nuxt.js):
- Better SEO and initial load times
- More complex deployment
- Consider for content-heavy sites
Performance Considerations
<!-- Lazy load heavy components -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue';
export default {
components: {
AsyncComponent: defineAsyncComponent(() => import('./HeavyComponent.vue'))
}
};
</script>
Common Pitfalls and Best Practices
State Management
- Use props and events for parent-child communication
- Consider Pinia for complex shared state
- Don’t overuse global state; most apps need less than you think
Component Design
- Keep components focused on a single responsibility
- Prefer composition over deep nesting
- Use slots for flexible component APIs
Performance
- Use
v-memofor expensive list items that rarely change - Avoid inline objects and functions in templates
- Consider
shallowReffor large objects you don’t need to track deeply
⚠️ Warning: Mutating props directly is an anti-pattern. Always emit events to update parent state.
Conclusion
Vue 3 strikes a balance between simplicity and power that makes it enjoyable to work with. The framework gives you escape hatches when you need them while keeping simple things simple.
Start with the Options API if you’re coming from other frameworks, then gradually adopt Composition API patterns as your applications grow. Focus on understanding reactivity and component communication patterns early, as these concepts underpin everything else.
The Vue ecosystem provides excellent tooling and libraries that handle common needs without forcing architectural decisions on you. This flexibility makes Vue particularly good for teams with varying experience levels and projects with evolving requirements.