Practical Guide to Developing with Vue 3
To debug Vue applications, use the Developer Tools panel acccessible by right-clicking in Chrome and selecting "Inspect" or pressing F12.
Run project scripts defined in package.json using npm or pnpm. For development, execute npm run serve (or pnpm dev) to start the server, and npm run build for production builds.
Configuring TypeScript
Errors in tsconfig.node.json, such as an invalid --lib option, often stem from a TypeScript version mismatch. Update TypeScript to the latest version:
npm install -D typescript@latest
Ensure trailing commas are removed within array configurations. For linting errors related to line endings (e.g., Delete ␍⏎), run the project's lint command:
npm run lint:eslint
Project Initialization
Install Node.js and configure npm. Use a package manager like pnpm for efficiency:
npm install -g pnpm
Create a new Vue 3 project using Vite, the recommended build tool:
npm init vue@latest
cd your-project
pnpm install
pnpm dev
The main entry points are main.js, App.vue, and the router configuration.
Composition API with <script setup>
The <script setup> syntax is the standard for Vue 3's Composition API, offering a concise syntax without the need for manual return statements.
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const emit = defineEmits(['update', 'remove'])
interface ComponentProps {
title: string;
isActive?: boolean;
}
const props = withDefaults(defineProps<ComponentProps>(), { title: 'Default' });
const handleClick = () => {
router.push('/home');
};
</script>
The setup() function serves as the entry point for the Composition API. It executes before the created lifecycle hook, and its this context is undefined.
Raective State Management
Use ref for primitive values and reactive for objects. ref returns a reactive reference object requiring .value access.
<script setup>
import { ref, reactive } from 'vue'
const counter = ref(0)
const increment = () => {
counter.value++
}
const userProfile = reactive({ name: 'Alex', score: 100 })
const updateScore = () => {
userProfile.score += 10
}
</script>
To destructure reactive object properties while maintaining reactivity, use toRefs.
<script setup>
import { reactive, toRefs } from 'vue';
const profile = reactive({ username: 'roy', points: 1000 });
const { username, points } = toRefs(profile);
function addPoints() {
points.value += 500;
}
</script>
Core Template Directives
- Event Handling (
v-onor@)<button @click="handleClick">Click</button> <button @click.stop="preventBubble">Stop Propagation</button> - Attribute Binding (
v-bindor:)<img :src="imageUrl"> <div :class="{ active: isActive }"></div> <div v-bind="multipleAttributes"></div> - Two-Way Binding (
v-model)<input v-model="userInput" /> - One-time Interpolation (
v-once)<span v-once>Static: {{ staticMessage }}</span> - Raw HTML (
v-html)<div v-html="rawHtmlContent"></div> - Conditional Rendering (
v-if)<p v-if="isVisible">This is conditionally shown.</p>
Component Communication
- Props (Parent to Child)
Parent component:
Child component:<template> <ChildComponent :item-data="itemData" :count="countValue" /> </template> <script setup> import { ref } from 'vue'; const countValue = ref(0); const itemData = { id: 1 }; </script><template> <div>{{ props.itemData.id }} - {{ count }}</div> </template> <script setup> const props = defineProps(['itemData', 'count']); </script> - Custom Events (Child to Parent)
Use
defineEmitsand the$emitmethod. - Global Event Bus
Utilize a third-party library like
mittfor cross-component communication. v-modelwith Components Implement two-way binding on custom components.useAttrsAccess non-prop attributes passed to a component.
Component Lifecycle and Properties
computed: Derives reactive state from other reactive sources.import { computed, ref } from 'vue'; const firstName = ref('John'); const lastName = ref('Doe'); const fullName = computed(() => `${firstName.value} ${lastName.value}`);methods: Define functions for event handlers or logic.watch: Perform side effects in reaction to state changes.import { watch, ref } from 'vue'; const searchTerm = ref(''); watch(searchTerm, (newVal, oldVal) => { console.log(`Term changed from ${oldVal} to ${newVal}`); // Perform search });mounted: Execute code after the component's DOM is rendered.
Routing Setup with Vue Router
Define routes in src/router/index.ts:
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import UserProfile from '@/views/UserProfile.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomeView },
{ path: '/user/:id', component: UserProfile, props: true },
{
path: '/settings',
component: SettingsLayout,
children: [
{ path: 'profile', component: ProfileComponent },
{ path: 'security', component: SecurityComponent }
]
}
]
});
export default router;
Use the router in components:
<template>
<nav>
<router-link to="/">Home</router-link>
<router-link :to="{ name: 'User', params: { id: userId } }">Profile</router-link>
</nav>
<router-view></router-view>
</template>
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
const userId = route.params.id;
</script>
Creating Custom Directives
Define a directive for role-based element control:
// directive/auth.ts
import { App, Directive, DirectiveBinding } from 'vue';
const permissionSet = new Set(['create', 'edit', 'view']);
const authDirective: Directive = {
mounted(el: Element, binding: DirectiveBinding) {
const requiredPermission = binding.value;
if (!permissionSet.has(requiredPermission)) {
el.parentNode?.removeChild(el);
}
}
};
export default {
install(app: App) {
app.directive('auth', authDirective);
}
};
Register it in main.ts:
import AuthDirective from './directive/auth';
app.use(AuthDirective);
Use in templates:
<button v-auth="'create'">Create</button>
<button v-auth="'delete'">Delete</button> <!-- Will be removed -->
TypeScript and Arrow Functions
Arrow functions provide concise syntax. When using generics in .tsx or .vue files with <script setup lang="ts">, a trailing comma may be required.
const identity = <T,>(value: T): T => value;
const processItem = <T extends Record<string, unknown>>(item: T): T => {
console.log(item);
return item;
};
Integrating UI Libraries
For Element Plus:
pnpm add element-plus @element-plus/icons-vue
Configure in main.ts:
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
const app = createApp(App);
app.use(ElementPlus);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount('#app');
Use components:
<template>
<el-button type="primary" :icon="Search">Search</el-button>
</template>
<script setup>
import { Search } from '@element-plus/icons-vue';
</script>
HTTP Client with Axios
Install Axios and create an instance with interceptors.
npm install axios
Basic usage:
import axios from 'axios';
axios.get('/api/data', { params: { id: 1 } })
.then(response => console.log(response.data))
.catch(error => console.error(error));
axios.post('/api/user', { name: 'John' })
.then(response => console.log(response.data));
Create a configured service instance:
// utils/request.js
import axios from 'axios';
import { ElMessage } from 'element-plus';
const httpClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
});
httpClient.interceptors.response.use(
response => {
const { data, meta } = response.data;
if (meta.status === 200) {
return data;
} else {
ElMessage.error(meta.msg);
return Promise.reject(new Error(meta.msg));
}
},
error => {
return Promise.reject(error);
}
);
export default httpClient;
API layer abstraction:
// api/user.js
import httpClient from '@/utils/request';
export function fetchUsers() {
return httpClient({ url: '/users' });
}
export function createUser(payload) {
return httpClient({
method: 'post',
url: '/users',
data: payload
});
}
State Management with Pinia
Pinia offers a simpler API compared to Vuex. Define a store:
// stores/user.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: 'Guest',
token: null
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
setUserInfo(payload) {
this.name = payload.name;
this.token = payload.token;
},
async fetchProfile() {
const data = await api.getProfile();
this.name = data.name;
}
}
});
Use in a component:
<template>
<div>Welcome, {{ user.name }}</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { name } = storeToRefs(userStore);
const login = () => {
userStore.setUserInfo({ name: 'Admin', token: 'abc123' });
};
</script>
For persistence, use pinia-plugin-persistedstate.
Browser Storage
localStorage: Persistent storage.localStorage.setItem('user', JSON.stringify(userData)); const user = JSON.parse(localStorage.getItem('user')); localStorage.removeItem('user');sessionStorage: Session-only storage.sessionStorage.setItem('formData', JSON.stringify(formState)); const data = JSON.parse(sessionStorage.getItem('formData'));
Environment Variables and Modes
Vite exposes environment variables prefixed with VITE_.
# .env.development
VITE_API_BASE_URL=/api
VITE_APP_TITLE=Development App
Access in code:
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const isProduction = import.meta.env.PROD;
In Webpack-based projects (Vue CLI), use VUE_APP_ prefix.
Type Assertions and Record Utility
- Type Assertion (
as)const element = document.getElementById('input') as HTMLInputElement; Record<K, T>Utility Type Defines an object type with keys of typeKand values of typeT.type Ingredient = 'flour' | 'sugar' | 'eggs'; const inventory: Record<Ingredient, number> = { flour: 100, sugar: 50, eggs: 12 };
Icon Integration
Use Iconify for a vast icon collection. Install the Vue integration:
pnpm add @iconify/vue
Create a wrapper component IconifyIcon.vue:
<!-- components/IconifyIcon.vue -->
<template>
<component :is="component" :icon="icon" v-bind="$attrs" />
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue';
const props = defineProps({
icon: { type: String, required: true }
});
const component = Icon;
</script>
Use it in templates:
<template>
<IconifyIcon icon="mdi:home" height="20" />
<el-button>
<IconifyIcon icon="ri:refresh-line" /> Refresh
</el-button>
</template>
<script setup>
import IconifyIcon from '@/components/IconifyIcon.vue';
</script>