Creating a Vue 3 SPA
Two recommended approaches exist for scaffolding a Vue 3 project:
- Vite – minimal, fast, exclusively for Vue 3.x
- Vue CLI – feature-rich, supports both Vue 2.x and Vue 3.x
Template Root Elements
Unlike Vue 2.x, Vue 3.x allows multiple root nodes inside a <template>. A wrapper <div> is no longer mandatory.
Dynamic Class Binding
Ternary expression for a single class
<h3 class='base' :class="isActive ? 'active' : ''">Content</h3>
<button @click='isActive = !isActive'>Toggle Active</button>
data() {
return { isActive: true }
}
Array syntax for multiple classes
<h3 class='base' :class="[isActive ? 'active' : '', isDeleted ? 'deleted' : '']">Content</h3>
data() {
return {
isActive: true,
isDeleted: false
}
}
Object syntax for multiple classes
<h3 class='base' :class='classStates'>Content</h3>
<button @click='classStates.active = !classStates.active'>Toggle Active</button>
<button @click='classStates.deleted = !classStates.deleted'>Toggle Deleted</button>
data() {
return {
classStates: {
active: true,
deleted: false
}
}
}
Inline Style Binding
Use an object to bind computed style values:
<template>
<div :style="{ backgroundColor: bg, fontSize: size + 'px' }">{{ label }}</div>
</template>
<script>
export default {
props: {
label: { type: String, default: 'Title' },
bg: { type: String, default: '#f0f0f0' },
size: { type: Number, default: 16 }
}
}
</script>
Prop Validation
Basic type checks
props: {
title: String,
count: Number,
isVisible: Boolean,
items: Array,
config: Object,
expiry: Date,
handler: Function,
symbol: Symbol
}
Custom validator
props: {
status: {
validator(value) {
// Must be one of the allowed states
return ['pending', 'approved', 'rejected'].includes(value)
}
}
}
Custom Events and emits
Vue 3 requires explicit declaration of emitted events.
<template>
<button @click='increment'>+1</button>
</template>
<script>
export default {
emits: ['counter-update'],
methods: {
increment() {
this.$emit('counter-update', this.currentCount)
}
}
}
</script>
Parent‑Child Two‑Way Data Sync
Using v-model with a custom prop:
<!-- Parent.vue -->
<ChildComponent v-model:value='parentValue'></ChildComponent>
<!-- Child.vue -->
<template>
<button @click='change'>Modify</button>
</template>
<script>
export default {
props: ['value'],
emits: ['update:value'],
methods: {
change() {
this.$emit('update:value', this.value + 10)
}
}
}
</script>
Sibling Communication with mitt
Vue 3 no longer has a built‑in EventBus. Use the lightweight mitt library instead.
npm install mitt
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();
Providing Data to Descendant Components
Non‑reactive data
// Ancestor.vue
export default {
data() {
return { themeColor: 'blue' }
},
provide() {
return {
sharedColor: this.themeColor
}
}
}
<!-- Descendant.vue -->
<template>
<h5>{{ sharedColor }}</h5>
</template>
<script>
export default {
inject: ['sharedColor']
}
</script>
Responsive data
Wrap the value with computed so that changes are reflected in all inejctors.
// Ancestor.vue
import { computed } from 'vue';
export default {
data() {
return { bgColor: 'white' }
},
provide() {
return {
responsiveBg: computed(() => this.bgColor)
}
}
}
<!-- Descendant.vue -->
<template>
<div :style="{ background: responsiveBg.value }">Content</div>
</template>
<script>
export default {
inject: ['responsiveBg']
}
</script>
Global Axios Configuration
Vue 2
// main.js
import Vue from 'vue';
import axios from 'axios';
axios.defaults.baseURL = 'https://api.example.com';
Vue.prototype.$http = axios;
Vue 3
// main.js
import { createApp } from 'vue';
import axios from 'axios';
import App from './App.vue';
const app = createApp(App);
axios.defaults.baseURL = 'https://api.example.com';
app.config.globalProperties.$http = axios;
app.mount('#app');
Usage inside a component:
methods: {
async fetchData() {
const { data } = await this.$http.get('/items', {
params: { page: 1 }
});
console.log(data);
}
}
Vue Router 4 Setup
import { createRouter, createWebHashHistory } from 'vue-router';
import HomeView from './Home.vue';
import MovieView from './Movie.vue';
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/home', component: HomeView },
{ path: '/movie', component: MovieView }
]
});
export default router;
Axios Interceptors
Request interceptor for authentciation
axios.interceptors.request.use(config => {
config.headers['Authorization'] = 'Bearer ' + getToken();
return config;
});
Loading indicator with Element UI
import { Loading } from 'element-ui';
let loadingInstance = null;
axios.interceptors.request.use(config => {
loadingInstance = Loading.service({ fullscreen: true });
return config;
});
axios.interceptors.response.use(
response => {
loadingInstance.close();
return response;
},
error => {
loadingInstance.close();
return Promise.reject(error);
}
);
Proxy for Cross‑Origin Requests
During development, devServer.proxy can forward API requests:
// vue.config.js or vite.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://backend.example.com',
changeOrigin: true
}
}
}
};
This is a development‑only convenience; in production the API server must enable CORS.